Add spring-batch-notion
This commit is contained in:
34
.github/workflows/spring-batch-notion.yml
vendored
Normal file
34
.github/workflows/spring-batch-notion.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Spring Batch Notion
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'spring-batch-notion/**'
|
||||
push:
|
||||
paths:
|
||||
- 'spring-batch-notion/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: spring-batch-notion
|
||||
|
||||
env:
|
||||
MAVEN_ARGS: -B -V -ntp -e -Djansi.passthrough=true -Dstyle.color=always
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
- name: Build with Maven
|
||||
run: ./mvnw $MAVEN_ARGS verify javadoc:javadoc
|
||||
53
README.md
53
README.md
@@ -6,13 +6,14 @@ This project is part of the [Spring organization][] on GitHub.
|
||||
|
||||
## Available Modules
|
||||
|
||||
| Module | Description | Lead | Version | CI build |
|
||||
|----------------------------|-------------------------------|------------------------------------------------------| ------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| spring-batch-excel | Support for [Microsoft Excel] | [@mdeinum](https://github.com/mdeinum) | 0.1.1 | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-excel.yml) |
|
||||
| spring-batch-elasticsearch | Support for [Elasticsearch] | [@parikshitdutta](https://github.com/parikshitdutta) | 0.1.0-SNAPSHOT | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-elasticsearch.yml) |
|
||||
| spring-batch-bigquery | Support for [Google BigQuery] | [@dgray16](https://github.com/dgray16) | 0.1.0 | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-bigquery.yml) |
|
||||
| spring-batch-neo4j | Support for [Neo4j] | [@michael-simons](https://github.com/michael-simons) | 0.1.0 | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-neo4j.yml) |
|
||||
| spring-batch-geode | Support for [Apache Geode] | TBA | 0.1.0-SNAPSHOT | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-geode.yml) |
|
||||
| Module | Description | Lead | Version | CI build |
|
||||
|--------------------------------------------------------------------|-------------------------------|------------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [spring-batch-bigquery](spring-batch-bigquery/README.adoc) | Support for [Google BigQuery] | [@dgray16](https://github.com/dgray16) | 0.1.0 | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-bigquery.yml) |
|
||||
| [spring-batch-elasticsearch](spring-batch-elasticsearch/README.md) | Support for [Elasticsearch] | [@parikshitdutta](https://github.com/parikshitdutta) | 0.1.0-SNAPSHOT | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-elasticsearch.yml) |
|
||||
| [spring-batch-excel](spring-batch-excel/README.adoc) | Support for [Microsoft Excel] | [@mdeinum](https://github.com/mdeinum) | 0.1.1 | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-excel.yml) |
|
||||
| [spring-batch-geode](spring-batch-geode/README.adoc) | Support for [Apache Geode] | TBA | 0.1.0-SNAPSHOT | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-geode.yml) |
|
||||
| [spring-batch-neo4j](spring-batch-neo4j/README.md) | Support for [Neo4j] | [@michael-simons](https://github.com/michael-simons) | 0.1.0 | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-neo4j.yml) |
|
||||
| [spring-batch-notion](spring-batch-notion/README.md) | Support for [Notion] | [@scordio](https://github.com/scordio) | 0.1.0-SNAPSHOT | [](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-notion.yml?query=branch%3Amain) |
|
||||
|
||||
## Getting support
|
||||
|
||||
@@ -92,6 +93,7 @@ Follow the Spring Batch team members and contributors on Twitter:
|
||||
* [@rotnroll666](https://twitter.com/rotnroll666) - Michael Simons
|
||||
* [@meistermeier](https://twitter.com/meistermeier) - Gerrit Meier
|
||||
* [@truecoder](https://twitter.com/truecoder) - Parikshit Dutta
|
||||
* [@stefanocodes](https://twitter.com/stefanocodes) - Stefano Cordio
|
||||
|
||||
## License
|
||||
|
||||
@@ -100,23 +102,24 @@ noted differently for individual extension Modules, but this should be the rare
|
||||
|
||||
**We look forward to your contributions!!**
|
||||
|
||||
[Spring Batch Project]: https://projects.spring.io/spring-batch/
|
||||
[Spring organization]: https://github.com/spring-projects
|
||||
[Microsoft Excel]: https://www.microsoft.com/en-us/microsoft-365/excel
|
||||
[Elasticsearch]: https://www.elastic.co
|
||||
[Google BigQuery]: https://cloud.google.com/bigquery
|
||||
[Neo4j]: https://neo4j.com
|
||||
[Apache Geode]: https://geode.apache.org
|
||||
[spring-batch tag]: https://stackoverflow.com/questions/tagged/spring-batch
|
||||
[Spring Batch]: https://github.com/spring-projects/spring-batch
|
||||
[Spring Boot]: https://github.com/spring-projects/spring-boot
|
||||
[Spring Cloud Task]: https://github.com/spring-cloud/spring-cloud-task
|
||||
[Spring Cloud Data Flow]: https://github.com/spring-cloud/spring-cloud-dataflow
|
||||
[GitHub Issue Tracker]: https://github.com/spring-projects/spring-batch-extensions/issues
|
||||
[Maven]: https://maven.apache.org
|
||||
[Git]: https://help.github.com/set-up-git-redirect
|
||||
[Spring Tool Suite]: https://spring.io/tools
|
||||
[Intellij IDEA]: https://www.jetbrains.com/idea/
|
||||
[Pull requests]: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests
|
||||
[Contributor Guidelines]: CONTRIBUTING.md
|
||||
[Apache License]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[Contributor Guidelines]: CONTRIBUTING.md
|
||||
[Elasticsearch]: https://www.elastic.co
|
||||
[Git]: https://help.github.com/set-up-git-redirect
|
||||
[GitHub Issue Tracker]: https://github.com/spring-projects/spring-batch-extensions/issues
|
||||
[Google BigQuery]: https://cloud.google.com/bigquery
|
||||
[Intellij IDEA]: https://www.jetbrains.com/idea/
|
||||
[Maven]: https://maven.apache.org
|
||||
[Microsoft Excel]: https://www.microsoft.com/en-us/microsoft-365/excel
|
||||
[Neo4j]: https://neo4j.com
|
||||
[Notion]: https://notion.so/
|
||||
[Pull requests]: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests
|
||||
[Spring Batch]: https://github.com/spring-projects/spring-batch
|
||||
[Spring Batch Project]: https://projects.spring.io/spring-batch/
|
||||
[Spring Boot]: https://github.com/spring-projects/spring-boot
|
||||
[Spring Cloud Data Flow]: https://github.com/spring-cloud/spring-cloud-dataflow
|
||||
[Spring Cloud Task]: https://github.com/spring-cloud/spring-cloud-task
|
||||
[Spring organization]: https://github.com/spring-projects
|
||||
[Spring Tool Suite]: https://spring.io/tools
|
||||
[spring-batch tag]: https://stackoverflow.com/questions/tagged/spring-batch
|
||||
|
||||
13
spring-batch-notion/.editorconfig
Normal file
13
spring-batch-notion/.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Maven POM code convention
|
||||
[pom.xml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
max_line_length = 205
|
||||
32
spring-batch-notion/.gitignore
vendored
Normal file
32
spring-batch-notion/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
### Maven ###
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
.flattened-pom.xml
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
19
spring-batch-notion/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
19
spring-batch-notion/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
97
spring-batch-notion/README.md
Normal file
97
spring-batch-notion/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Spring Batch Notion [](https://mvnrepository.com/artifact/org.springframework.batch.extensions/spring-batch-notion) [](https://javadoc.io/doc/org.springframework.batch.extensions/spring-batch-notion)
|
||||
|
||||
[](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-notion.yml?query=branch%3Amain)
|
||||
|
||||
This project provides a [Spring Batch][] extension module that adds support for [Notion][].
|
||||
|
||||
## Compatibility
|
||||
|
||||
Spring Batch Notion is based on Spring Batch 5 and tested on Spring Boot 3, thus requiring at least Java 17.
|
||||
|
||||
Compatibility is guaranteed only with the Spring Boot versions under [OSS support](https://spring.io/projects/spring-boot/#support).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Maven
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.batch.extensions</groupId>
|
||||
<artifactId>spring-batch-notion</artifactId>
|
||||
<version>${spring-batch-notion.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
|
||||
```kotlin
|
||||
implementation("org.springframework.batch.extensions:spring-batch-notion:${springBatchNotionVersion}")
|
||||
```
|
||||
|
||||
## NotionDatabaseItemReader
|
||||
|
||||
The `NotionDatabaseItemReader` is a restartable `ItemReader` that reads entries from a [Notion Database] via a paging technique.
|
||||
|
||||
A minimal configuration of the item reader is as follows:
|
||||
|
||||
```java
|
||||
NotionDatabaseItemReader<Item> itemReader() {
|
||||
NotionDatabaseItemReader<Item> reader = new NotionDatabaseItemReader<>();
|
||||
reader.setToken(System.getenv("NOTION_TOKEN"));
|
||||
reader.setDatabaseId("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"); // UUID
|
||||
reader.setPropertiesMapper(new CustomPropertyMapper());
|
||||
return reader;
|
||||
}
|
||||
```
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
| Property | Required | Default | Description |
|
||||
|------------------|----------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| `baseUrl` | no | `https://api.notion.com/v1` | Base URL of the Notion API. A custom value can be provided for testing purposes (e.g., the URL of a [WireMock][] server). |
|
||||
| `databaseId` | yes | - | UUID of the database to read from. |
|
||||
| `filter` | no | `null` | `Filter` condition to limit the returned items. |
|
||||
| `pageSize` | no | `100` | Number of items to be read with each page. Must be greater than zero and less than or equal to 100. |
|
||||
| `propertyMapper` | yes | - | The `PropertyMapper` responsible for mapping properties of a Notion item into a Java object. |
|
||||
| `sorts` | no | `null` | `Sort` conditions to order the returned items. Each condition is applied following the declaration order. |
|
||||
| `token` | yes | - | The Notion integration token. |
|
||||
|
||||
In addition to the Notion-specific configuration, all the configuration options of the Spring Batch
|
||||
[`AbstractPaginatedDataItemReader`](https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.html)
|
||||
are supported.
|
||||
|
||||
### PropertyMapper
|
||||
|
||||
The `NotionDatabaseItemReader` requires a `PropertyMapper` to map the properties of a Notion item into an object.
|
||||
|
||||
Currently, only properties of type [Title](https://developers.notion.com/reference/property-object#title)
|
||||
and [Rich Text](https://developers.notion.com/reference/property-object#rich-text) are supported,
|
||||
and both are converted to strings.
|
||||
|
||||
The following `PropertyMapper` implementations are provided out of the box.
|
||||
|
||||
| Name | Description |
|
||||
|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `BeanWrapperPropertyMapper` | Supports JavaBeans. Requires a default constructor and expects the setter names to match the Notion item property names (case-insensitive). |
|
||||
| `ConstructorPropertyMapper` | Supports types with a constructor with arguments. Requires the constructor to be unique and its argument names to match the Notion item property names (case-insensitive). |
|
||||
| `RecordPropertyMapper` | Supports Java records. It uses the canonical constructor and requires the component names to match the Notion item property names (case-insensitive). |
|
||||
|
||||
All implementations above offer two constructors:
|
||||
* One accepting the `Class` instance of the type to be mapped
|
||||
* One without parameters, for cases where the type to be mapped can be inferred by the generic type of the variable or method enclosing the constructor declaration
|
||||
|
||||
In case none of the provided implementations is suitable, a custom one can be provided.
|
||||
|
||||
## NotionDatabaseItemWriter
|
||||
|
||||
Currently not provided but will be added in the future.
|
||||
|
||||
## License
|
||||
|
||||
The Spring Batch Notion is released under version 2.0 of the [Apache License][].
|
||||
|
||||
[Apache License]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[Notion]: https://notion.so/
|
||||
[Notion Database]: https://www.notion.so/help/category/databases
|
||||
[Spring Batch]: https://github.com/spring-projects/spring-batch
|
||||
[WireMock]: https://wiremock.org/
|
||||
259
spring-batch-notion/mvnw
vendored
Executable file
259
spring-batch-notion/mvnw
vendored
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
149
spring-batch-notion/mvnw.cmd
vendored
Normal file
149
spring-batch-notion/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
227
spring-batch-notion/pom.xml
Normal file
227
spring-batch-notion/pom.xml
Normal file
@@ -0,0 +1,227 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>org.springframework.batch.extensions</groupId>
|
||||
<artifactId>spring-batch-notion</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
|
||||
<name>Spring Batch Notion</name>
|
||||
<description>Spring Batch extension for Notion</description>
|
||||
<url>https://github.com/spring-projects/spring-batch-extensions/spring-batch-notion</url>
|
||||
<inceptionYear>2024</inceptionYear>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<id>scordio</id>
|
||||
<name>Stefano Cordio</name>
|
||||
<email>stefano.cordio@gmail.com</email>
|
||||
<url>https://github.com/scordio</url>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-batch-extensions/spring-batch-notion</url>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<archunit.version>1.3.0</archunit.version>
|
||||
<notion-sdk-jvm.version>1.11.1</notion-sdk-jvm.version>
|
||||
<wiremock-spring-boot.version>3.0.3</wiremock-spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Compile -->
|
||||
<dependency>
|
||||
<groupId>com.github.seratch</groupId>
|
||||
<artifactId>notion-sdk-jvm-core</artifactId>
|
||||
<version>${notion-sdk-jvm.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.seratch</groupId>
|
||||
<artifactId>notion-sdk-jvm-httpclient</artifactId>
|
||||
<version>${notion-sdk-jvm.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.seratch</groupId>
|
||||
<artifactId>notion-sdk-jvm-slf4j2</artifactId>
|
||||
<version>${notion-sdk-jvm.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-beans</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.batch</groupId>
|
||||
<artifactId>spring-batch-infrastructure</artifactId>
|
||||
</dependency>
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<version>${archunit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.batch</groupId>
|
||||
<artifactId>spring-batch-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-batch</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.wiremock.integrations</groupId>
|
||||
<artifactId>wiremock-spring-boot</artifactId>
|
||||
<version>${wiremock-spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<version>4.6</version>
|
||||
<configuration>
|
||||
<licenseSets>
|
||||
<licenseSet>
|
||||
<inlineHeader><![CDATA[
|
||||
Copyright 2002-${year} the original author or 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.
|
||||
]]></inlineHeader>
|
||||
<includes>
|
||||
<include>src/**/*.java</include>
|
||||
</includes>
|
||||
</licenseSet>
|
||||
</licenseSets>
|
||||
<properties>
|
||||
<year>2024</year>
|
||||
</properties>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.spring.javaformat</groupId>
|
||||
<artifactId>spring-javaformat-maven-plugin</artifactId>
|
||||
<version>0.0.43</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>validate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<configuration>
|
||||
<additionalDependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
</additionalDependencies>
|
||||
<failOnWarnings>true</failOnWarnings>
|
||||
<links>
|
||||
<link>https://docs.spring.io/spring-batch/docs/${spring-batch.version}/api/</link>
|
||||
<link>https://docs.spring.io/spring-framework/docs/${spring-framework.version}/api/</link>
|
||||
</links>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-source</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<flattenMode>ossrh</flattenMode>
|
||||
<pomElements>
|
||||
<profiles>remove</profiles>
|
||||
</pomElements>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten-clean</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,615 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion;
|
||||
|
||||
import notion.api.v1.model.databases.query.filter.CompoundFilterElement;
|
||||
import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.CheckboxFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.MultiSelectFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.NumberFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.SelectFilter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Filtering conditions to limit the entries returned from a database query.
|
||||
* <p>
|
||||
* Filters operate on property values or entry timestamps, and can be combined.
|
||||
* <p>
|
||||
* A filter definition starts with {@link #where()}, entry point for a fluent API that
|
||||
* mimics the database filter option in the Notion UI.
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
public abstract sealed class Filter {
|
||||
|
||||
/**
|
||||
* Entry point that starts the definition of a filter.
|
||||
* @return a new {@link FilterConditionBuilder} instance
|
||||
*/
|
||||
public static FilterConditionBuilder<TopLevelFilter> where() {
|
||||
return new FilterConditionBuilder<>(PropertyFilter::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point that starts the definition of a filter group.
|
||||
* @param filter the filter representing the group
|
||||
* @return a new {@link TopLevelFilter} instance that delegates to the given filter
|
||||
*/
|
||||
public static TopLevelFilter where(Filter filter) {
|
||||
return new DelegateFilter(filter);
|
||||
}
|
||||
|
||||
private Filter() {
|
||||
}
|
||||
|
||||
abstract QueryTopLevelFilter toQueryTopLevelFilter();
|
||||
|
||||
abstract CompoundFilterElement toCompoundFilterElement();
|
||||
|
||||
/**
|
||||
* Base class for top level filters that support filters composition via the
|
||||
* {@link TopLevelFilter#and} and {@link TopLevelFilter#or} methods.
|
||||
*
|
||||
* @see AndFilter
|
||||
* @see OrFilter
|
||||
*/
|
||||
public static abstract sealed class TopLevelFilter extends Filter {
|
||||
|
||||
private TopLevelFilter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of a new filter that is composed with the current filter
|
||||
* via a logical {@code and}.
|
||||
* @return a {@link FilterConditionBuilder} instance for an {@link AndFilter}
|
||||
*/
|
||||
public FilterConditionBuilder<AndFilter> and() {
|
||||
return new FilterConditionBuilder<>(
|
||||
(property, customizer) -> new AndFilter(this, new PropertyFilter(property, customizer)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the current filter and the given filter via a logical {@code and}.
|
||||
* @param filter the filter to compose with the current filter
|
||||
* @return a new {@link AndFilter} instance
|
||||
*/
|
||||
public AndFilter and(Filter filter) {
|
||||
return new AndFilter(this, Objects.requireNonNull(filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of a new filter that is composed with the current filter
|
||||
* via a logical {@code or}.
|
||||
* @return a {@link FilterConditionBuilder} instance for an {@link OrFilter}
|
||||
*/
|
||||
public FilterConditionBuilder<OrFilter> or() {
|
||||
return new FilterConditionBuilder<>(
|
||||
(property, customizer) -> new OrFilter(this, new PropertyFilter(property, customizer)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the current filter and the given filter via a logical {@code or}.
|
||||
* @param filter the filter to compose with the current filter
|
||||
* @return a new {@link OrFilter} instance
|
||||
*/
|
||||
public OrFilter or(Filter filter) {
|
||||
return new OrFilter(this, Objects.requireNonNull(filter));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class DelegateFilter extends TopLevelFilter {
|
||||
|
||||
private final Filter delegate;
|
||||
|
||||
private DelegateFilter(Filter delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
QueryTopLevelFilter toQueryTopLevelFilter() {
|
||||
return delegate.toQueryTopLevelFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
CompoundFilterElement toCompoundFilterElement() {
|
||||
return delegate.toCompoundFilterElement();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class PropertyFilter extends TopLevelFilter {
|
||||
|
||||
private final String property;
|
||||
|
||||
private final NotionPropertyFilterCustomizer customizer;
|
||||
|
||||
private PropertyFilter(String property, NotionPropertyFilterCustomizer customizer) {
|
||||
this.property = property;
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
QueryTopLevelFilter toQueryTopLevelFilter() {
|
||||
return toNotionPropertyFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
CompoundFilterElement toCompoundFilterElement() {
|
||||
return toNotionPropertyFilter();
|
||||
}
|
||||
|
||||
private notion.api.v1.model.databases.query.filter.PropertyFilter toNotionPropertyFilter() {
|
||||
var notionPropertyFilter = new notion.api.v1.model.databases.query.filter.PropertyFilter(property);
|
||||
customizer.accept(notionPropertyFilter);
|
||||
return notionPropertyFilter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface NotionPropertyFilterFactory<T extends Filter>
|
||||
extends BiFunction<String, NotionPropertyFilterCustomizer, T> {
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface NotionPropertyFilterCustomizer
|
||||
extends Consumer<notion.api.v1.model.databases.query.filter.PropertyFilter> {
|
||||
|
||||
}
|
||||
|
||||
static abstract sealed class CompoundFilter extends Filter {
|
||||
|
||||
final List<Filter> filters = new ArrayList<>();
|
||||
|
||||
private final NotionCompoundFilterSetter setter;
|
||||
|
||||
private CompoundFilter(NotionCompoundFilterSetter setter) {
|
||||
this.setter = setter;
|
||||
}
|
||||
|
||||
@Override
|
||||
QueryTopLevelFilter toQueryTopLevelFilter() {
|
||||
return toNotionCompoundFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
CompoundFilterElement toCompoundFilterElement() {
|
||||
return toNotionCompoundFilter();
|
||||
}
|
||||
|
||||
private notion.api.v1.model.databases.query.filter.CompoundFilter toNotionCompoundFilter() {
|
||||
var notionCompoundFilter = new notion.api.v1.model.databases.query.filter.CompoundFilter();
|
||||
var notionCompoundFilterElements = filters.stream().map(Filter::toCompoundFilterElement).toList();
|
||||
setter.accept(notionCompoundFilter, notionCompoundFilterElements);
|
||||
return notionCompoundFilter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface NotionCompoundFilterSetter
|
||||
extends BiConsumer<notion.api.v1.model.databases.query.filter.CompoundFilter, List<CompoundFilterElement>> {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compound filter that supports filters composition via the {@link AndFilter#and}
|
||||
* methods.
|
||||
* <p>
|
||||
* Returns entries that match <b>all</b> of the provided filters.
|
||||
*/
|
||||
public static final class AndFilter extends CompoundFilter {
|
||||
|
||||
private AndFilter(Filter first, Filter second) {
|
||||
super(notion.api.v1.model.databases.query.filter.CompoundFilter::setAnd);
|
||||
filters.addAll(List.of(first, second));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of a new filter that is composed with the current filter
|
||||
* via a logical {@code and}.
|
||||
* @return a {@link FilterConditionBuilder} instance for an {@link AndFilter}
|
||||
*/
|
||||
public FilterConditionBuilder<AndFilter> and() {
|
||||
return new FilterConditionBuilder<>((property, customizer) -> {
|
||||
filters.add(new PropertyFilter(property, customizer));
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the current filter and the given filter via a logical {@code and}.
|
||||
* @param filter the filter to compose with the current filter
|
||||
* @return a new {@link AndFilter} instance
|
||||
*/
|
||||
public AndFilter and(Filter filter) {
|
||||
filters.add(Objects.requireNonNull(filter));
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compound filter that supports filters composition via the {@link OrFilter#or}
|
||||
* methods.
|
||||
* <p>
|
||||
* Returns entries that match <b>any</b> of the provided filters.
|
||||
*/
|
||||
public static final class OrFilter extends CompoundFilter {
|
||||
|
||||
private OrFilter(Filter first, Filter second) {
|
||||
super(notion.api.v1.model.databases.query.filter.CompoundFilter::setOr);
|
||||
filters.addAll(List.of(first, second));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of a new filter that is composed with the current filter
|
||||
* via a logical {@code or}.
|
||||
* @return a {@link FilterConditionBuilder} instance for an {@link OrFilter}
|
||||
*/
|
||||
public FilterConditionBuilder<OrFilter> or() {
|
||||
return new FilterConditionBuilder<>((property, customizer) -> {
|
||||
filters.add(new PropertyFilter(property, customizer));
|
||||
return this;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the current filter and the given filter via a logical {@code or}.
|
||||
* @param filter the filter to compose with the current filter
|
||||
* @return a new {@link OrFilter} instance
|
||||
*/
|
||||
public OrFilter or(Filter filter) {
|
||||
filters.add(Objects.requireNonNull(filter));
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link Filter} conditions.
|
||||
*
|
||||
* @param <T> the type of the target {@code Filter}
|
||||
*/
|
||||
public static final class FilterConditionBuilder<T extends Filter> {
|
||||
|
||||
private final NotionPropertyFilterFactory<T> factory;
|
||||
|
||||
private FilterConditionBuilder(NotionPropertyFilterFactory<T> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of the filter condition for a {@code checkbox} property.
|
||||
* @param property The name of the property as it appears in the database, or the
|
||||
* property ID
|
||||
* @return a new {@link CheckboxCondition} instance
|
||||
*/
|
||||
public CheckboxCondition<T> checkbox(String property) {
|
||||
return new CheckboxCondition<>(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of the filter condition for a {@code multi-select}
|
||||
* property.
|
||||
* @param property The name of the property as it appears in the database, or the
|
||||
* property ID
|
||||
* @return a new {@link MultiSelectCondition} instance
|
||||
*/
|
||||
public MultiSelectCondition<T> multiSelect(String property) {
|
||||
return new MultiSelectCondition<>(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of the filter condition for a {@code number} property.
|
||||
* @param property The name of the property as it appears in the database, or the
|
||||
* property ID
|
||||
* @return a new {@link NumberCondition} instance
|
||||
*/
|
||||
public NumberCondition<T> number(String property) {
|
||||
return new NumberCondition<>(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the definition of the filter condition for a {@code select} property.
|
||||
* @param property The name of the property as it appears in the database, or the
|
||||
* property ID
|
||||
* @return a new {@link SelectCondition} instance
|
||||
*/
|
||||
public SelectCondition<T> select(String property) {
|
||||
return new SelectCondition<>(property, factory);
|
||||
}
|
||||
|
||||
static abstract sealed class Condition<T extends Filter> {
|
||||
|
||||
private final String property;
|
||||
|
||||
private final NotionPropertyFilterFactory<T> factory;
|
||||
|
||||
private Condition(String property, NotionPropertyFilterFactory<T> factory) {
|
||||
this.property = property;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
T toFilter(NotionPropertyFilterCustomizer customizer) {
|
||||
return factory.apply(property, customizer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter condition for a {@code checkbox} property.
|
||||
*
|
||||
* @param <T> the type of the target filter
|
||||
*/
|
||||
public static final class CheckboxCondition<T extends Filter> extends Condition<T> {
|
||||
|
||||
private CheckboxCondition(String property, NotionPropertyFilterFactory<T> factory) {
|
||||
super(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all database entries with an exact value match.
|
||||
* @param value the value to match
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isEqualTo(boolean value) {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setCheckbox(checkboxFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all database entries without an exact value match.
|
||||
* @param value the value to differ with
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isNotEqualTo(boolean value) {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setDoesNotEqual(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setCheckbox(checkboxFilter));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter condition for a {@code multi-select} property.
|
||||
*
|
||||
* @param <T> the type of the target filter
|
||||
*/
|
||||
public static final class MultiSelectCondition<T extends Filter> extends Condition<T> {
|
||||
|
||||
private MultiSelectCondition(String property, NotionPropertyFilterFactory<T> factory) {
|
||||
super(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the provided value is part of the property
|
||||
* values.
|
||||
* @param value the value to compare the property values against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T contains(String value) {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setContains(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the provided value is not contained in the
|
||||
* property values.
|
||||
* @param value the value to compare the property values against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T doesNotContain(String value) {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setDoesNotContain(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value does not contain any data.
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isEmpty() {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setEmpty(true);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value contains data.
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isNotEmpty() {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setNotEmpty(true);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter condition for a {@code number} property.
|
||||
*
|
||||
* @param <T> the type of the target filter
|
||||
*/
|
||||
public static final class NumberCondition<T extends Filter> extends Condition<T> {
|
||||
|
||||
private NumberCondition(String property, NotionPropertyFilterFactory<T> factory) {
|
||||
super(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value is the same as the
|
||||
* provided one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isEqualTo(int value) {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setEquals(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value differs from the provided
|
||||
* one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isNotEqualTo(int value) {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setDoesNotEqual(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value exceeds the provided one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isGreaterThan(int value) {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setGreaterThan(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value is equal to or exceeds the
|
||||
* provided one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isGreaterThanOrEqualTo(int value) {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setGreaterThanOrEqualTo(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value is less than the provided
|
||||
* one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isLessThan(int value) {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setLessThan(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value is equal to or is less
|
||||
* than the provided one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isLessThanOrEqualTo(int value) {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setLessThanOrEqualTo(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value does not contain any data.
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isEmpty() {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setEmpty(true);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value contains data.
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isNotEmpty() {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setNotEmpty(true);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter condition for a {@code select} property.
|
||||
*
|
||||
* @param <T> the type of the target filter
|
||||
*/
|
||||
public static final class SelectCondition<T extends Filter> extends Condition<T> {
|
||||
|
||||
private SelectCondition(String property, NotionPropertyFilterFactory<T> factory) {
|
||||
super(property, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value matches the provided one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isEqualTo(String value) {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEquals(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value does not match the
|
||||
* provided one.
|
||||
* @param value the value to compare the property value against
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isNotEqualTo(String value) {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setDoesNotEqual(value);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value does not contain any data.
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isEmpty() {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEmpty(true);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return database entries where the property value contains data.
|
||||
* @return a filter with the newly defined condition
|
||||
*/
|
||||
public T isNotEmpty() {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion;
|
||||
|
||||
import org.springframework.batch.extensions.notion.mapping.PropertyMapper;
|
||||
import notion.api.v1.NotionClient;
|
||||
import notion.api.v1.http.JavaNetHttpClient;
|
||||
import notion.api.v1.logging.Slf4jLogger;
|
||||
import notion.api.v1.model.databases.QueryResults;
|
||||
import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter;
|
||||
import notion.api.v1.model.databases.query.sort.QuerySort;
|
||||
import notion.api.v1.model.pages.Page;
|
||||
import notion.api.v1.model.pages.PageProperty;
|
||||
import notion.api.v1.model.pages.PageProperty.RichText;
|
||||
import notion.api.v1.request.databases.QueryDatabaseRequest;
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.batch.item.ItemReader;
|
||||
import org.springframework.batch.item.data.AbstractPaginatedDataItemReader;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Restartable {@link ItemReader} that reads entries from a Notion database via a paging
|
||||
* technique.
|
||||
* <p>
|
||||
* The query is executed using paged requests of a size specified in
|
||||
* {@link #setPageSize(int)}, which defaults to {@value #DEFAULT_PAGE_SIZE}. Additional
|
||||
* pages are requested as needed when the {@link #read()} method is called. On restart,
|
||||
* the reader will begin again at the same number item it left off at.
|
||||
* <p>
|
||||
* This implementation is thread-safe between calls to {@link #open(ExecutionContext)},
|
||||
* but remember to set <code>saveState</code> to <code>false</code> if used in a
|
||||
* multi-threaded environment (no restart available).
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
* @param <T> Type of item to be read
|
||||
*/
|
||||
public class NotionDatabaseItemReader<T> extends AbstractPaginatedDataItemReader<T> implements InitializingBean {
|
||||
|
||||
private static final String DEFAULT_BASE_URL = "https://api.notion.com/v1";
|
||||
|
||||
private static final int DEFAULT_PAGE_SIZE = 100;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
private String token;
|
||||
|
||||
private String databaseId;
|
||||
|
||||
private PropertyMapper<T> propertyMapper;
|
||||
|
||||
private QueryTopLevelFilter filter;
|
||||
|
||||
private List<QuerySort> sorts;
|
||||
|
||||
private NotionClient client;
|
||||
|
||||
private boolean hasMore;
|
||||
|
||||
private String nextCursor;
|
||||
|
||||
/**
|
||||
* Create a new {@link NotionDatabaseItemReader} with the following defaults:
|
||||
* <ul>
|
||||
* <li>{@code baseUrl} = {@value #DEFAULT_BASE_URL}</li>
|
||||
* <li>{@code pageSize} = {@value #DEFAULT_PAGE_SIZE}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public NotionDatabaseItemReader() {
|
||||
this.baseUrl = DEFAULT_BASE_URL;
|
||||
this.pageSize = DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base URL of the Notion API.
|
||||
* <p>
|
||||
* Defaults to {@value #DEFAULT_BASE_URL}.
|
||||
* <p>
|
||||
* A custom value can be provided for testing purposes (e.g., the URL of a WireMock
|
||||
* server).
|
||||
* @param baseUrl the base URL
|
||||
*/
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = Objects.requireNonNull(baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Notion integration token.
|
||||
* <p>
|
||||
* Always required.
|
||||
* @param token the token
|
||||
*/
|
||||
public void setToken(String token) {
|
||||
this.token = Objects.requireNonNull(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID of the database to read from.
|
||||
* <p>
|
||||
* Always required.
|
||||
* @param databaseId the database UUID
|
||||
*/
|
||||
public void setDatabaseId(String databaseId) {
|
||||
this.databaseId = Objects.requireNonNull(databaseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link PropertyMapper} responsible for mapping Notion item properties into a
|
||||
* Java object.
|
||||
* <p>
|
||||
* Always required.
|
||||
* @param propertyMapper the property mapper
|
||||
*/
|
||||
public void setPropertyMapper(PropertyMapper<T> propertyMapper) {
|
||||
this.propertyMapper = Objects.requireNonNull(propertyMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Filter} condition to limit the returned items.
|
||||
* <p>
|
||||
* If no filter is provided, all the items in the database will be returned.
|
||||
* @param filter the {@link Filter} conditions
|
||||
* @see Filter#where()
|
||||
* @see Filter#where(Filter)
|
||||
*/
|
||||
public void setFilter(Filter filter) {
|
||||
this.filter = filter.toQueryTopLevelFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Sort} conditions to order the returned items.
|
||||
* <p>
|
||||
* Each condition is applied following the declaration order, i.e., earlier sorts take
|
||||
* precedence over later ones.
|
||||
* @param sorts the {@link Sort} conditions
|
||||
* @see Sort#by(String)
|
||||
* @see Sort#by(Sort.Timestamp)
|
||||
*/
|
||||
public void setSorts(Sort... sorts) {
|
||||
this.sorts = Stream.of(sorts).map(Sort::toQuerySort).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of items to be read with each page.
|
||||
* <p>
|
||||
* Defaults to {@value #DEFAULT_PAGE_SIZE}.
|
||||
* @param pageSize the number of items. Must be greater than 0 and less than or equal
|
||||
* to 100.
|
||||
*/
|
||||
@Override
|
||||
public void setPageSize(int pageSize) {
|
||||
Assert.isTrue(pageSize <= 100, "pageSize must be less than or equal to 100");
|
||||
super.setPageSize(pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Iterator<T> doPageRead() {
|
||||
if (!hasMore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
QueryDatabaseRequest request = new QueryDatabaseRequest(databaseId);
|
||||
request.setFilter(filter);
|
||||
request.setSorts(sorts);
|
||||
request.setStartCursor(nextCursor);
|
||||
request.setPageSize(pageSize);
|
||||
|
||||
QueryResults queryResults = client.queryDatabase(request);
|
||||
|
||||
hasMore = queryResults.getHasMore();
|
||||
nextCursor = queryResults.getNextCursor();
|
||||
|
||||
return queryResults.getResults()
|
||||
.stream()
|
||||
.map(NotionDatabaseItemReader::getProperties)
|
||||
.map(properties -> propertyMapper.map(properties))
|
||||
.iterator();
|
||||
}
|
||||
|
||||
private static Map<String, String> getProperties(Page element) {
|
||||
return element.getProperties()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toUnmodifiableMap(Entry::getKey, entry -> getPropertyValue(entry.getValue())));
|
||||
}
|
||||
|
||||
private static String getPropertyValue(PageProperty property) {
|
||||
return switch (property.getType()) {
|
||||
case RichText -> getPlainText(property.getRichText());
|
||||
case Title -> getPlainText(property.getTitle());
|
||||
default -> throw new IllegalArgumentException("Unsupported type: " + property.getType());
|
||||
};
|
||||
}
|
||||
|
||||
private static String getPlainText(List<RichText> texts) {
|
||||
return texts.isEmpty() ? "" : texts.get(0).getPlainText();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void doOpen() {
|
||||
client = new NotionClient(token);
|
||||
client.setHttpClient(new JavaNetHttpClient());
|
||||
client.setLogger(new Slf4jLogger());
|
||||
client.setBaseUrl(baseUrl);
|
||||
|
||||
hasMore = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void doClose() {
|
||||
client.close();
|
||||
client = null;
|
||||
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void jumpToItem(int itemIndex) throws Exception {
|
||||
for (int i = 0; i < itemIndex; i++) {
|
||||
read();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.state(token != null, "'token' must be set");
|
||||
Assert.state(databaseId != null, "'databaseId' must be set");
|
||||
Assert.state(propertyMapper != null, "'propertyMapper' must be set");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion;
|
||||
|
||||
import notion.api.v1.model.databases.query.sort.QuerySort;
|
||||
import notion.api.v1.model.databases.query.sort.QuerySortDirection;
|
||||
import notion.api.v1.model.databases.query.sort.QuerySortTimestamp;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Sort conditions to order the entries returned from a database query.
|
||||
* <p>
|
||||
* Sorts operate on property values or entry timestamps, and can be combined.
|
||||
* <p>
|
||||
* The direction defaults to {@link Direction#DEFAULT_DIRECTION}.
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
public abstract sealed class Sort {
|
||||
|
||||
/**
|
||||
* Default direction of {@link Sort} conditions.
|
||||
*/
|
||||
public static final Direction DEFAULT_DIRECTION = Direction.ASCENDING;
|
||||
|
||||
/**
|
||||
* Sort condition that orders the database query by a particular property.
|
||||
* @param property the name of the property to sort against
|
||||
* @param direction the {@code Direction} to sort
|
||||
* @return the {@code Sort} instance
|
||||
*/
|
||||
public static Sort by(String property, Direction direction) {
|
||||
return new PropertySort(property, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort condition that orders the database query by a particular property, in
|
||||
* ascending direction.
|
||||
* @param property the name of the property to sort against
|
||||
* @return the {@code Sort} instance
|
||||
*/
|
||||
public static Sort by(String property) {
|
||||
return new PropertySort(property, DEFAULT_DIRECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort condition that orders the database query by the timestamp associated with a
|
||||
* database entry.
|
||||
* @param timestamp the {@code Timestamp} to sort against
|
||||
* @param direction the {@code Direction} to sort
|
||||
* @return the {@code Sort} instance
|
||||
*/
|
||||
public static Sort by(Timestamp timestamp, Direction direction) {
|
||||
return new TimestampSort(timestamp, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort condition that orders the database query by the timestamp associated with a
|
||||
* database entry, in ascending direction.
|
||||
* @param timestamp the {@code Timestamp timestamp} to sort against
|
||||
* @return the {@code Sort} instance
|
||||
*/
|
||||
public static Sort by(Timestamp timestamp) {
|
||||
return new TimestampSort(timestamp, DEFAULT_DIRECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps associated with database entries.
|
||||
*/
|
||||
public enum Timestamp {
|
||||
|
||||
/**
|
||||
* The time the entry was created.
|
||||
*/
|
||||
CREATED_TIME(QuerySortTimestamp.CreatedTime),
|
||||
|
||||
/**
|
||||
* The time the entry was last edited.
|
||||
*/
|
||||
LAST_EDITED_TIME(QuerySortTimestamp.LastEditedTime);
|
||||
|
||||
private final QuerySortTimestamp querySortTimestamp;
|
||||
|
||||
Timestamp(QuerySortTimestamp querySortTimestamp) {
|
||||
this.querySortTimestamp = querySortTimestamp;
|
||||
}
|
||||
|
||||
private QuerySortTimestamp getQuerySortTimestamp() {
|
||||
return querySortTimestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort directions.
|
||||
*/
|
||||
public enum Direction {
|
||||
|
||||
/**
|
||||
* Ascending direction.
|
||||
*/
|
||||
ASCENDING(QuerySortDirection.Ascending),
|
||||
|
||||
/**
|
||||
* Descending direction.
|
||||
*/
|
||||
DESCENDING(QuerySortDirection.Descending);
|
||||
|
||||
private final QuerySortDirection querySortDirection;
|
||||
|
||||
Direction(QuerySortDirection querySortDirection) {
|
||||
this.querySortDirection = querySortDirection;
|
||||
}
|
||||
|
||||
private QuerySortDirection getQuerySortDirection() {
|
||||
return querySortDirection;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Sort() {
|
||||
}
|
||||
|
||||
abstract QuerySort toQuerySort();
|
||||
|
||||
private static final class PropertySort extends Sort {
|
||||
|
||||
private final String property;
|
||||
|
||||
private final Direction direction;
|
||||
|
||||
private PropertySort(String property, Direction direction) {
|
||||
this.property = Objects.requireNonNull(property);
|
||||
this.direction = Objects.requireNonNull(direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
QuerySort toQuerySort() {
|
||||
return new QuerySort(property, null, direction.getQuerySortDirection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "%s: %s".formatted(property, direction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TimestampSort extends Sort {
|
||||
|
||||
private final Timestamp timestamp;
|
||||
|
||||
private final Direction direction;
|
||||
|
||||
private TimestampSort(Timestamp timestamp, Direction direction) {
|
||||
this.timestamp = Objects.requireNonNull(timestamp);
|
||||
this.direction = Objects.requireNonNull(direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
QuerySort toQuerySort() {
|
||||
return new QuerySort(null, timestamp.getQuerySortTimestamp(), direction.getQuerySortDirection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "%s: %s".formatted(timestamp, direction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.beans.PropertyAccessorFactory;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
/**
|
||||
* {@link PropertyMapper} implementation for JavaBeans.
|
||||
* <p>
|
||||
* It requires a default constructor and expects the setter names to match the Notion item
|
||||
* property names (case-insensitive).
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
* @param <T> the target type
|
||||
*/
|
||||
public class BeanWrapperPropertyMapper<T> extends CaseInsensitivePropertyMapper<T> {
|
||||
|
||||
private final Constructor<T> constructor;
|
||||
|
||||
/**
|
||||
* Create a new {@link BeanWrapperPropertyMapper} for the given target type.
|
||||
* @param type type of the target object
|
||||
*/
|
||||
public BeanWrapperPropertyMapper(Class<T> type) {
|
||||
this.constructor = BeanUtils.getResolvableConstructor(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link BeanWrapperPropertyMapper}, inferring the target type.
|
||||
* @param reified don't pass any values to it. It's a trick to detect the target type.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public BeanWrapperPropertyMapper(T... reified) {
|
||||
this(ClassResolver.getClassOf(reified));
|
||||
}
|
||||
|
||||
@Override
|
||||
T mapCaseInsensitive(LinkedCaseInsensitiveMap<String> properties) {
|
||||
T instance = BeanUtils.instantiateClass(constructor);
|
||||
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(instance);
|
||||
beanWrapper.setPropertyValues(properties);
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
abstract class CaseInsensitivePropertyMapper<T> implements PropertyMapper<T> {
|
||||
|
||||
@Override
|
||||
public T map(Map<String, String> properties) {
|
||||
LinkedCaseInsensitiveMap<String> caseInsensitiveProperties = new LinkedCaseInsensitiveMap<>(properties.size());
|
||||
caseInsensitiveProperties.putAll(properties);
|
||||
return mapCaseInsensitive(caseInsensitiveProperties);
|
||||
}
|
||||
|
||||
abstract T mapCaseInsensitive(LinkedCaseInsensitiveMap<String> properties);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class ClassResolver {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Class<T> getClassOf(T[] reified) {
|
||||
Assert.isTrue(reified.length == 0,
|
||||
"Please don't pass any values here. The type will be detected automagically.");
|
||||
|
||||
return (Class<T>) reified.getClass().getComponentType();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
abstract class ConstructorBasedPropertyMapper<T> extends CaseInsensitivePropertyMapper<T> {
|
||||
|
||||
private final Constructor<T> constructor;
|
||||
|
||||
ConstructorBasedPropertyMapper(Class<T> type) {
|
||||
try {
|
||||
this.constructor = getConstructor(type);
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
abstract Constructor<T> getConstructor(Class<T> type) throws NoSuchMethodException;
|
||||
|
||||
@Override
|
||||
T mapCaseInsensitive(LinkedCaseInsensitiveMap<String> properties) {
|
||||
Object[] parameterValues = Arrays.stream(constructor.getParameters()) //
|
||||
.map(Parameter::getName) //
|
||||
.map(properties::get) //
|
||||
.toArray();
|
||||
|
||||
return BeanUtils.instantiateClass(constructor, parameterValues);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* {@link PropertyMapper} implementation for types with a constructor with arguments.
|
||||
* <p>
|
||||
* It requires the constructor to be unique and its parameter names to match the Notion
|
||||
* item property names (case-insensitive).
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
* @param <T> the target type
|
||||
*/
|
||||
public class ConstructorPropertyMapper<T> extends ConstructorBasedPropertyMapper<T> {
|
||||
|
||||
/**
|
||||
* Create a new {@link ConstructorPropertyMapper} for the given target type.
|
||||
* @param type type of the target object
|
||||
*/
|
||||
public ConstructorPropertyMapper(Class<T> type) {
|
||||
super(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ConstructorPropertyMapper}, inferring the target type.
|
||||
* @param reified don't pass any values to it. It's a trick to detect the target type.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public ConstructorPropertyMapper(T... reified) {
|
||||
this(ClassResolver.getClassOf(reified));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
Constructor<T> getConstructor(Class<T> type) throws NoSuchMethodException {
|
||||
Constructor<?>[] constructors = type.getDeclaredConstructors();
|
||||
|
||||
if (constructors.length == 0) {
|
||||
throw new NoSuchMethodException("No constructor found for type: " + type);
|
||||
}
|
||||
|
||||
if (constructors.length > 1) {
|
||||
throw new NoSuchMethodException("Multiple constructors available: " + Arrays.toString(constructors));
|
||||
}
|
||||
|
||||
return (Constructor<T>) constructors[0];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Strategy interface for mapping the properties of a Notion item into a Java object.
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
* @param <T> the object type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PropertyMapper<T> {
|
||||
|
||||
/**
|
||||
* Map the given item properties into an object of type {@code T}.
|
||||
* @param properties unmodifiable map containing the property value objects, keyed by
|
||||
* property name
|
||||
* @return the populated object
|
||||
*/
|
||||
T map(Map<String, String> properties);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* {@link PropertyMapper} implementation for {@link Record Java records}.
|
||||
* <p>
|
||||
* It uses the record's canonical constructor and requires the record's component names to
|
||||
* match the Notion item property names (case-insensitive).
|
||||
*
|
||||
* @author Stefano Cordio
|
||||
* @param <T> the target type — must be a {@link Record}
|
||||
*/
|
||||
public class RecordPropertyMapper<T extends Record> extends ConstructorBasedPropertyMapper<T> {
|
||||
|
||||
/**
|
||||
* Create a new {@link RecordPropertyMapper} for the given target type.
|
||||
* @param type type of the target record
|
||||
*/
|
||||
public RecordPropertyMapper(Class<T> type) {
|
||||
super(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RecordPropertyMapper}, inferring the target type.
|
||||
* @param reified don't pass any values to it. It's a trick to detect the target type.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public RecordPropertyMapper(T... reified) {
|
||||
this(ClassResolver.getClassOf(reified));
|
||||
}
|
||||
|
||||
@Override
|
||||
Constructor<T> getConstructor(Class<T> type) throws NoSuchMethodException {
|
||||
Class<?>[] parameterTypes = Arrays.stream(type.getRecordComponents()) //
|
||||
.map(RecordComponent::getType) //
|
||||
.toArray(Class[]::new);
|
||||
|
||||
return ReflectionUtils.accessibleConstructor(type, parameterTypes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion;
|
||||
|
||||
import notion.api.v1.model.databases.query.filter.CompoundFilter;
|
||||
import notion.api.v1.model.databases.query.filter.PropertyFilter;
|
||||
import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.CheckboxFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.MultiSelectFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.NumberFilter;
|
||||
import notion.api.v1.model.databases.query.filter.condition.SelectFilter;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.batch.extensions.notion.Filter.where;
|
||||
import static java.util.function.Function.identity;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class FilterTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource({ "propertyFilters", "compoundFilters", "nestedFilters" })
|
||||
void toQueryTopLevelFilter(Filter underTest, QueryTopLevelFilter expected) {
|
||||
// WHEN
|
||||
QueryTopLevelFilter result = underTest.toQueryTopLevelFilter();
|
||||
// THEN
|
||||
then(result).usingRecursiveComparison().isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> propertyFilters() {
|
||||
return Stream.of( //
|
||||
checkboxFilters(), //
|
||||
multiSelectFilters(), //
|
||||
numberFilters(), //
|
||||
selectFilters()) //
|
||||
.flatMap(identity());
|
||||
}
|
||||
|
||||
static Stream<Arguments> checkboxFilters() {
|
||||
return Stream.of(true, false)
|
||||
.flatMap(value -> Stream.of( //
|
||||
arguments( //
|
||||
where().checkbox("property").isEqualTo(value), //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(value);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().checkbox("property").isNotEqualTo(value), //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setDoesNotEqual(value);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}))));
|
||||
}
|
||||
|
||||
static Stream<Arguments> multiSelectFilters() {
|
||||
return Stream.of( //
|
||||
arguments( //
|
||||
where().multiSelect("property").contains("value"), //
|
||||
supply(() -> {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setContains("value");
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setMultiSelect(multiSelectFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().multiSelect("property").doesNotContain("value"), //
|
||||
supply(() -> {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setDoesNotContain("value");
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setMultiSelect(multiSelectFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().multiSelect("property").isEmpty(), //
|
||||
supply(() -> {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setMultiSelect(multiSelectFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().multiSelect("property").isNotEmpty(), //
|
||||
supply(() -> {
|
||||
MultiSelectFilter multiSelectFilter = new MultiSelectFilter();
|
||||
multiSelectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setMultiSelect(multiSelectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
}
|
||||
|
||||
static Stream<Arguments> numberFilters() {
|
||||
return Stream.of( //
|
||||
arguments( //
|
||||
where().number("property").isEqualTo(42), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setEquals(42);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isNotEqualTo(42), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setDoesNotEqual(42);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isGreaterThan(42), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setGreaterThan(42);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isGreaterThanOrEqualTo(42), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setGreaterThanOrEqualTo(42);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isLessThan(42), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setLessThan(42);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isLessThanOrEqualTo(42), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setLessThanOrEqualTo(42);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isEmpty(), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().number("property").isNotEmpty(), //
|
||||
supply(() -> {
|
||||
NumberFilter numberFilter = new NumberFilter();
|
||||
numberFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setNumber(numberFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
}
|
||||
|
||||
static Stream<Arguments> selectFilters() {
|
||||
return Stream.of( //
|
||||
arguments( //
|
||||
where().select("property").isEqualTo("value"), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEquals("value");
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().select("property").isNotEqualTo("value"), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setDoesNotEqual("value");
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().select("property").isEmpty(), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( //
|
||||
where().select("property").isNotEmpty(), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("property");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
}
|
||||
|
||||
static Stream<Arguments> compoundFilters() {
|
||||
return Stream.of(andFilters(), orFilters()).flatMap(identity());
|
||||
}
|
||||
|
||||
static Stream<Arguments> andFilters() {
|
||||
return Stream.of( //
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(false)
|
||||
.and().select("another").isNotEmpty(), //
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setAnd(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})),
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(false)
|
||||
.and(where().select("another").isNotEmpty()),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setAnd(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})),
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(false)
|
||||
.and(where().select("another").isNotEmpty())
|
||||
.and().checkbox("one-more").isNotEqualTo(true)
|
||||
.and(where().select("another-more").isEmpty()),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setAnd(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setDoesNotEqual(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("one-more");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another-more");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})));
|
||||
}
|
||||
|
||||
static Stream<Arguments> orFilters() {
|
||||
return Stream.of( //
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(false)
|
||||
.or().select("another").isNotEmpty(),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setOr(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})),
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(false)
|
||||
.or(where().select("another").isNotEmpty()),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setOr(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})),
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(false)
|
||||
.or(where().select("another").isNotEmpty())
|
||||
.or().checkbox("one-more").isNotEqualTo(true)
|
||||
.or(where().select("another-more").isEmpty()),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setOr(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setDoesNotEqual(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("one-more");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another-more");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})));
|
||||
}
|
||||
|
||||
static Stream<Arguments> nestedFilters() {
|
||||
return Stream.of( //
|
||||
arguments( // @formatter:off
|
||||
where(where().checkbox("active").isEqualTo(true)),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
})),
|
||||
arguments( // @formatter:off
|
||||
where().checkbox("active").isEqualTo(true)
|
||||
.and(where().select("another").isEmpty().or().select("another").isEqualTo("value")),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setAnd(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
CompoundFilter innerFilter = new CompoundFilter();
|
||||
|
||||
innerFilter.setOr(List.of( //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEquals("value");
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return innerFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})),
|
||||
arguments( // @formatter:off
|
||||
where(where().checkbox("active").isEqualTo(false)
|
||||
.or().select("another").isNotEmpty())
|
||||
.and().select("one-more").isEmpty(),
|
||||
// @formatter:on
|
||||
supply(() -> {
|
||||
CompoundFilter compoundFilter = new CompoundFilter();
|
||||
|
||||
compoundFilter.setAnd(List.of( //
|
||||
supply(() -> {
|
||||
CompoundFilter innerFilter = new CompoundFilter();
|
||||
|
||||
innerFilter.setOr(List.of( //
|
||||
supply(() -> {
|
||||
CheckboxFilter checkboxFilter = new CheckboxFilter();
|
||||
checkboxFilter.setEquals(false);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("active");
|
||||
propertyFilter.setCheckbox(checkboxFilter);
|
||||
return propertyFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setNotEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("another");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return innerFilter;
|
||||
}), //
|
||||
supply(() -> {
|
||||
SelectFilter selectFilter = new SelectFilter();
|
||||
selectFilter.setEmpty(true);
|
||||
PropertyFilter propertyFilter = new PropertyFilter("one-more");
|
||||
propertyFilter.setSelect(selectFilter);
|
||||
return propertyFilter;
|
||||
})));
|
||||
|
||||
return compoundFilter;
|
||||
})));
|
||||
}
|
||||
|
||||
private static <T> T supply(Supplier<T> supplier) {
|
||||
return supplier.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
|
||||
import static com.tngtech.archunit.base.DescribedPredicate.anyElementThat;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
@AnalyzeClasses(packagesOf = NotionDatabaseItemReader.class)
|
||||
class NotionJvmSdkTests {
|
||||
|
||||
private static final DescribedPredicate<JavaClass> RESIDE_IN_NOTION_JVM_SDK_PACKAGE = //
|
||||
resideInAPackage("notion.api..");
|
||||
|
||||
@ArchTest
|
||||
void library_types_should_not_be_exposed(JavaClasses classes) {
|
||||
// @formatter:off
|
||||
ArchRule rule = methods()
|
||||
.that().arePublic().or().areProtected()
|
||||
.should().notHaveRawReturnType(RESIDE_IN_NOTION_JVM_SDK_PACKAGE)
|
||||
.andShould().notHaveRawParameterTypes(anyElementThat(RESIDE_IN_NOTION_JVM_SDK_PACKAGE));
|
||||
// @formatter:on
|
||||
rule.check(classes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion;
|
||||
|
||||
import notion.api.v1.model.databases.query.sort.QuerySort;
|
||||
import notion.api.v1.model.databases.query.sort.QuerySortDirection;
|
||||
import notion.api.v1.model.databases.query.sort.QuerySortTimestamp;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.batch.extensions.notion.Sort.Direction.ASCENDING;
|
||||
import static org.springframework.batch.extensions.notion.Sort.Direction.DESCENDING;
|
||||
import static org.springframework.batch.extensions.notion.Sort.Timestamp.CREATED_TIME;
|
||||
import static org.springframework.batch.extensions.notion.Sort.Timestamp.LAST_EDITED_TIME;
|
||||
import static notion.api.v1.model.databases.query.sort.QuerySortDirection.Ascending;
|
||||
import static notion.api.v1.model.databases.query.sort.QuerySortDirection.Descending;
|
||||
import static notion.api.v1.model.databases.query.sort.QuerySortTimestamp.CreatedTime;
|
||||
import static notion.api.v1.model.databases.query.sort.QuerySortTimestamp.LastEditedTime;
|
||||
import static org.assertj.core.api.BDDAssertions.from;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class SortTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void toQuerySort(Sort underTest, String property, QuerySortTimestamp timestamp, QuerySortDirection direction) {
|
||||
// WHEN
|
||||
QuerySort result = underTest.toQuerySort();
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns(direction, from(QuerySort::getDirection))
|
||||
.returns(property, from(QuerySort::getProperty))
|
||||
.returns(timestamp, from(QuerySort::getTimestamp));
|
||||
}
|
||||
|
||||
static Stream<Arguments> toQuerySort() {
|
||||
return Stream.of( //
|
||||
arguments(Sort.by("property"), "property", null, Ascending),
|
||||
arguments(Sort.by("property", ASCENDING), "property", null, Ascending),
|
||||
arguments(Sort.by("property", DESCENDING), "property", null, Descending),
|
||||
arguments(Sort.by(CREATED_TIME), null, CreatedTime, Ascending),
|
||||
arguments(Sort.by(CREATED_TIME, ASCENDING), null, CreatedTime, Ascending),
|
||||
arguments(Sort.by(CREATED_TIME, DESCENDING), null, CreatedTime, Descending),
|
||||
arguments(Sort.by(LAST_EDITED_TIME), null, LastEditedTime, Ascending),
|
||||
arguments(Sort.by(LAST_EDITED_TIME, ASCENDING), null, LastEditedTime, Ascending),
|
||||
arguments(Sort.by(LAST_EDITED_TIME, DESCENDING), null, LastEditedTime, Descending));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testToString(Sort underTest, String expected) {
|
||||
// WHEN
|
||||
String result = underTest.toString();
|
||||
// THEN
|
||||
then(result).isEqualTo(expected);
|
||||
}
|
||||
|
||||
static Stream<Arguments> testToString() {
|
||||
return Stream.of( //
|
||||
arguments(Sort.by("property"), "property: ASCENDING"),
|
||||
arguments(Sort.by("property", ASCENDING), "property: ASCENDING"),
|
||||
arguments(Sort.by("property", DESCENDING), "property: DESCENDING"),
|
||||
arguments(Sort.by(CREATED_TIME), "CREATED_TIME: ASCENDING"),
|
||||
arguments(Sort.by(CREATED_TIME, ASCENDING), "CREATED_TIME: ASCENDING"),
|
||||
arguments(Sort.by(CREATED_TIME, DESCENDING), "CREATED_TIME: DESCENDING"),
|
||||
arguments(Sort.by(LAST_EDITED_TIME), "LAST_EDITED_TIME: ASCENDING"),
|
||||
arguments(Sort.by(LAST_EDITED_TIME, ASCENDING), "LAST_EDITED_TIME: ASCENDING"),
|
||||
arguments(Sort.by(LAST_EDITED_TIME, DESCENDING), "LAST_EDITED_TIME: DESCENDING"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.it;
|
||||
|
||||
import org.springframework.batch.test.context.SpringBatchTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.wiremock.spring.EnableWireMock;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
@Target(TYPE)
|
||||
@Retention(RUNTIME)
|
||||
@SpringBootTest(properties = "spring.batch.job.enabled=false")
|
||||
@SpringBatchTest
|
||||
@EnableWireMock
|
||||
public @interface IntegrationTest {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.it;
|
||||
|
||||
import org.springframework.batch.extensions.notion.Sort.Direction;
|
||||
import org.springframework.batch.extensions.notion.Sort.Timestamp;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
public class RequestBodies {
|
||||
|
||||
public static String queryRequest(int pageSize, JSONObject... sorts) {
|
||||
return queryRequest(null, pageSize, sorts);
|
||||
}
|
||||
|
||||
public static String queryRequest(UUID startCursor, int pageSize, JSONObject... sorts) {
|
||||
try {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
if (sorts.length > 0) {
|
||||
jsonObject.put("sorts", new JSONArray(sorts));
|
||||
}
|
||||
|
||||
return jsonObject //
|
||||
.put("page_size", pageSize)
|
||||
.putOpt("start_cursor", startCursor != null ? startCursor.toString() : null)
|
||||
.toString();
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JSONObject sortByProperty(String property, Direction direction) {
|
||||
try {
|
||||
return new JSONObject() //
|
||||
.put("property", property)
|
||||
.put("direction", direction.name().toLowerCase());
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JSONObject sortByTimestamp(String property, Timestamp timestamp) {
|
||||
try {
|
||||
return new JSONObject() //
|
||||
.put("property", property)
|
||||
.put("timestamp", timestamp.name().toLowerCase());
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.it;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
public class RequestHeaders {
|
||||
|
||||
public static final String NOTION_VERSION = "Notion-Version";
|
||||
|
||||
public static final String NOTION_VERSION_VALUE = "2022-06-28";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.it;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.UUID.randomUUID;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
public class ResponseBodies {
|
||||
|
||||
public static String queryResponse(JSONObject... results) {
|
||||
return queryResponse(null, results);
|
||||
}
|
||||
|
||||
public static String queryResponse(UUID nextCursor, JSONObject... results) {
|
||||
try {
|
||||
return new JSONObject() //
|
||||
.put("object", "list")
|
||||
.put("results", new JSONArray(results))
|
||||
.put("next_cursor", nextCursor != null ? nextCursor.toString() : null)
|
||||
.put("has_more", nextCursor != null)
|
||||
.put("type", "page")
|
||||
.put("page", new JSONObject())
|
||||
.toString();
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JSONObject result(UUID id, UUID databaseId, Map<?, ?> properties) {
|
||||
try {
|
||||
Instant now = Instant.now();
|
||||
|
||||
return new JSONObject() //
|
||||
.put("object", "page")
|
||||
.put("id", id.toString())
|
||||
.put("created_time", now.toString())
|
||||
.put("last_edited_time", now.toString())
|
||||
.put("created_by", new JSONObject())
|
||||
.put("last_edited_by", new JSONObject())
|
||||
.put("parent", new JSONObject() //
|
||||
.put("type", "database_id")
|
||||
.put("database_id", databaseId.toString()))
|
||||
.put("archived", false)
|
||||
.put("properties", new JSONObject(properties))
|
||||
.put("url", "https://www.notion.so/" + randomUUID().toString().replace("-", ""));
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JSONObject title(String value) {
|
||||
try {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
|
||||
if (value != null) {
|
||||
jsonArray.put(new JSONObject() //
|
||||
.put("type", "text")
|
||||
.put("text", new JSONObject() //
|
||||
.put("content", value))
|
||||
.put("annotations", new JSONObject() //
|
||||
.put("bold", false)
|
||||
.put("italic", false)
|
||||
.put("strikethrough", false)
|
||||
.put("underline", false)
|
||||
.put("code", false)
|
||||
.put("color", "default"))
|
||||
.put("plain_text", value));
|
||||
}
|
||||
|
||||
return new JSONObject() //
|
||||
.put("id", "title")
|
||||
.put("type", "title")
|
||||
.put("title", jsonArray);
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JSONObject richText(String value) {
|
||||
try {
|
||||
return new JSONObject() //
|
||||
.put("id", "JV%3B%3F")
|
||||
.put("type", "rich_text")
|
||||
.put("rich_text", new JSONArray() //
|
||||
.put(new JSONObject() //
|
||||
.put("type", "text")
|
||||
.put("text", new JSONObject() //
|
||||
.put("content", value))
|
||||
.put("annotations", new JSONObject() //
|
||||
.put("bold", false)
|
||||
.put("italic", false)
|
||||
.put("strikethrough", false)
|
||||
.put("underline", false)
|
||||
.put("code", false)
|
||||
.put("color", "default"))
|
||||
.put("plain_text", value)));
|
||||
}
|
||||
catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.it.pagination;
|
||||
|
||||
import org.springframework.batch.extensions.notion.NotionDatabaseItemReader;
|
||||
import org.springframework.batch.extensions.notion.Sort;
|
||||
import org.springframework.batch.extensions.notion.it.IntegrationTest;
|
||||
import org.springframework.batch.extensions.notion.mapping.RecordPropertyMapper;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.batch.item.support.ListItemWriter;
|
||||
import org.springframework.batch.test.JobLauncherTestUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static org.springframework.batch.extensions.notion.Sort.Direction.DESCENDING;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestBodies.queryRequest;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestBodies.sortByProperty;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestHeaders.NOTION_VERSION;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestHeaders.NOTION_VERSION_VALUE;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.queryResponse;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.result;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.richText;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.title;
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
|
||||
import static org.springframework.batch.core.ExitStatus.COMPLETED;
|
||||
import static wiremock.com.google.common.net.HttpHeaders.AUTHORIZATION;
|
||||
import static wiremock.com.google.common.net.HttpHeaders.CONTENT_TYPE;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
@IntegrationTest
|
||||
class MultiplePagesDescendingTests {
|
||||
|
||||
private static final UUID DATABASE_ID = randomUUID();
|
||||
|
||||
private static final int PAGE_SIZE = 2;
|
||||
|
||||
@Autowired
|
||||
JobLauncherTestUtils launcher;
|
||||
|
||||
@Autowired
|
||||
ListItemWriter<PaginatedDescendingJob.Item> itemWriter;
|
||||
|
||||
@Test
|
||||
void should_succeed() throws Exception {
|
||||
// GIVEN
|
||||
UUID thirdResultId = randomUUID();
|
||||
|
||||
JSONObject firstResult = result(randomUUID(), DATABASE_ID,
|
||||
Map.of("Name", title("Name string"), "Value", richText("123456")));
|
||||
JSONObject secondResult = result(randomUUID(), DATABASE_ID,
|
||||
Map.of("Name", title("Another name string"), "Value", richText("0987654321")));
|
||||
JSONObject thirdResult = result(thirdResultId, DATABASE_ID,
|
||||
Map.of("Name", title(""), "Value", richText("abc-1234")));
|
||||
|
||||
givenThat(post("/databases/%s/query".formatted(DATABASE_ID)) //
|
||||
.withHeader(AUTHORIZATION, matching("Bearer .+"))
|
||||
.withHeader(CONTENT_TYPE, containing("application/json"))
|
||||
.withHeader(NOTION_VERSION, equalTo(NOTION_VERSION_VALUE))
|
||||
.withRequestBody(equalToJson(queryRequest(PAGE_SIZE, sortByProperty("Name", DESCENDING))))
|
||||
.willReturn(okJson(queryResponse(thirdResultId, firstResult, secondResult))));
|
||||
|
||||
givenThat(post("/databases/%s/query".formatted(DATABASE_ID)) //
|
||||
.withHeader(AUTHORIZATION, matching("Bearer .+"))
|
||||
.withHeader(CONTENT_TYPE, containing("application/json"))
|
||||
.withHeader(NOTION_VERSION, equalTo(NOTION_VERSION_VALUE))
|
||||
.withRequestBody(equalToJson(queryRequest(thirdResultId, PAGE_SIZE, sortByProperty("Name", DESCENDING))))
|
||||
.willReturn(okJson(queryResponse(thirdResult))));
|
||||
|
||||
// WHEN
|
||||
JobExecution jobExecution = launcher.launchJob();
|
||||
|
||||
// THEN
|
||||
then(jobExecution.getExitStatus()).isEqualTo(COMPLETED);
|
||||
|
||||
then(itemWriter.getWrittenItems()).asInstanceOf(LIST)
|
||||
.containsExactly( //
|
||||
new PaginatedDescendingJob.Item("Name string", "123456"), //
|
||||
new PaginatedDescendingJob.Item("Another name string", "0987654321"), //
|
||||
new PaginatedDescendingJob.Item("", "abc-1234"));
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
static class PaginatedDescendingJob {
|
||||
|
||||
@Value("${wiremock.server.baseUrl}")
|
||||
private String wiremockBaseUrl;
|
||||
|
||||
@Bean
|
||||
Job job(JobRepository jobRepository, Step step) {
|
||||
return new JobBuilder("TEST-JOB", jobRepository).start(step).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
|
||||
return new StepBuilder("TEST-STEP", jobRepository) //
|
||||
.<Item, Item>chunk(PAGE_SIZE, transactionManager) //
|
||||
.reader(itemReader()) //
|
||||
.writer(itemWriter()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
NotionDatabaseItemReader<Item> itemReader() {
|
||||
NotionDatabaseItemReader<Item> reader = new NotionDatabaseItemReader<>();
|
||||
|
||||
reader.setSaveState(false);
|
||||
|
||||
reader.setToken("token");
|
||||
reader.setBaseUrl(wiremockBaseUrl);
|
||||
reader.setDatabaseId(DATABASE_ID.toString());
|
||||
|
||||
reader.setPageSize(PAGE_SIZE);
|
||||
reader.setSorts(Sort.by("Name", DESCENDING));
|
||||
reader.setPropertyMapper(new RecordPropertyMapper<>());
|
||||
|
||||
return reader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ListItemWriter<Item> itemWriter() {
|
||||
return new ListItemWriter<>();
|
||||
}
|
||||
|
||||
record Item(String name, String value) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.it.pagination;
|
||||
|
||||
import org.springframework.batch.extensions.notion.NotionDatabaseItemReader;
|
||||
import org.springframework.batch.extensions.notion.it.IntegrationTest;
|
||||
import org.springframework.batch.extensions.notion.mapping.RecordPropertyMapper;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.batch.item.support.ListItemWriter;
|
||||
import org.springframework.batch.test.JobLauncherTestUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestBodies.queryRequest;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestHeaders.NOTION_VERSION;
|
||||
import static org.springframework.batch.extensions.notion.it.RequestHeaders.NOTION_VERSION_VALUE;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.queryResponse;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.result;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.richText;
|
||||
import static org.springframework.batch.extensions.notion.it.ResponseBodies.title;
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
|
||||
import static org.springframework.batch.core.ExitStatus.COMPLETED;
|
||||
import static wiremock.com.google.common.net.HttpHeaders.AUTHORIZATION;
|
||||
import static wiremock.com.google.common.net.HttpHeaders.CONTENT_TYPE;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
@IntegrationTest
|
||||
class MultiplePagesTests {
|
||||
|
||||
private static final UUID DATABASE_ID = randomUUID();
|
||||
|
||||
private static final int PAGE_SIZE = 2;
|
||||
|
||||
@Autowired
|
||||
JobLauncherTestUtils launcher;
|
||||
|
||||
@Autowired
|
||||
ListItemWriter<PaginatedJob.Item> itemWriter;
|
||||
|
||||
@Test
|
||||
void should_succeed() throws Exception {
|
||||
// GIVEN
|
||||
UUID thirdResultId = randomUUID();
|
||||
|
||||
JSONObject firstResult = result(randomUUID(), DATABASE_ID,
|
||||
Map.of("Name", title("Another name string"), "Value", richText("0987654321")));
|
||||
JSONObject secondResult = result(randomUUID(), DATABASE_ID,
|
||||
Map.of("Name", title("Name string"), "Value", richText("123456")));
|
||||
JSONObject thirdResult = result(thirdResultId, DATABASE_ID,
|
||||
Map.of("Name", title(""), "Value", richText("abc-1234")));
|
||||
|
||||
givenThat(post("/databases/%s/query".formatted(DATABASE_ID)) //
|
||||
.withHeader(AUTHORIZATION, matching("Bearer .+"))
|
||||
.withHeader(CONTENT_TYPE, containing("application/json"))
|
||||
.withHeader(NOTION_VERSION, equalTo(NOTION_VERSION_VALUE))
|
||||
.withRequestBody(equalToJson(queryRequest(PAGE_SIZE)))
|
||||
.willReturn(okJson(queryResponse(thirdResultId, firstResult, secondResult))));
|
||||
|
||||
givenThat(post("/databases/%s/query".formatted(DATABASE_ID)) //
|
||||
.withHeader(AUTHORIZATION, matching("Bearer .+"))
|
||||
.withHeader(CONTENT_TYPE, containing("application/json"))
|
||||
.withHeader(NOTION_VERSION, equalTo(NOTION_VERSION_VALUE))
|
||||
.withRequestBody(equalToJson(queryRequest(thirdResultId, PAGE_SIZE)))
|
||||
.willReturn(okJson(queryResponse(thirdResult))));
|
||||
|
||||
// WHEN
|
||||
JobExecution jobExecution = launcher.launchJob();
|
||||
|
||||
// THEN
|
||||
then(jobExecution.getExitStatus()).isEqualTo(COMPLETED);
|
||||
|
||||
then(itemWriter.getWrittenItems()).asInstanceOf(LIST)
|
||||
.containsExactly( //
|
||||
new PaginatedJob.Item("Another name string", "0987654321"), //
|
||||
new PaginatedJob.Item("Name string", "123456"), //
|
||||
new PaginatedJob.Item("", "abc-1234"));
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
static class PaginatedJob {
|
||||
|
||||
@Value("${wiremock.server.baseUrl}")
|
||||
private String wiremockBaseUrl;
|
||||
|
||||
@Bean
|
||||
Job job(JobRepository jobRepository, Step step) {
|
||||
return new JobBuilder("TEST-JOB", jobRepository).start(step).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
|
||||
return new StepBuilder("TEST-STEP", jobRepository) //
|
||||
.<Item, Item>chunk(PAGE_SIZE, transactionManager) //
|
||||
.reader(itemReader()) //
|
||||
.writer(itemWriter()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
NotionDatabaseItemReader<Item> itemReader() {
|
||||
NotionDatabaseItemReader<Item> reader = new NotionDatabaseItemReader<>();
|
||||
|
||||
reader.setSaveState(false);
|
||||
|
||||
reader.setToken("token");
|
||||
reader.setBaseUrl(wiremockBaseUrl);
|
||||
reader.setDatabaseId(DATABASE_ID.toString());
|
||||
|
||||
reader.setPageSize(PAGE_SIZE);
|
||||
reader.setPropertyMapper(new RecordPropertyMapper<>());
|
||||
|
||||
return reader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ListItemWriter<Item> itemWriter() {
|
||||
return new ListItemWriter<>();
|
||||
}
|
||||
|
||||
record Item(String name, String value) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.batch.extensions.notion.mapping.TestData.AllPropertiesSource;
|
||||
import org.springframework.batch.extensions.notion.mapping.TestData.PartialPropertiesSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.catchThrowable;
|
||||
import static org.assertj.core.api.BDDAssertions.from;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class BeanWrapperPropertyMapperTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestBean> underTest = new BeanWrapperPropertyMapper<>(TestBean.class);
|
||||
// WHEN
|
||||
TestBean result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestBean::getField1)) //
|
||||
.returns("Value2", from(TestBean::getField2));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@PartialPropertiesSource
|
||||
void should_map_partial_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestBean> underTest = new BeanWrapperPropertyMapper<>(TestBean.class);
|
||||
// WHEN
|
||||
TestBean result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestBean::getField1)) //
|
||||
.returns(null, from(TestBean::getField2));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties_without_type_parameter(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestBean> underTest = new BeanWrapperPropertyMapper<>();
|
||||
// WHEN
|
||||
TestBean result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestBean::getField1)) //
|
||||
.returns("Value2", from(TestBean::getField2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_fail_with_vararg_constructor_parameter() {
|
||||
// WHEN
|
||||
Throwable thrown = catchThrowable(() -> new BeanWrapperPropertyMapper<>(new TestBean()));
|
||||
// THEN
|
||||
then(thrown) //
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Please don't pass any values here. The type will be detected automagically.");
|
||||
}
|
||||
|
||||
private static class TestBean {
|
||||
|
||||
private String field1;
|
||||
|
||||
private String field2;
|
||||
|
||||
public String getField1() {
|
||||
return field1;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setField1(String field1) {
|
||||
this.field1 = field1;
|
||||
}
|
||||
|
||||
public String getField2() {
|
||||
return field2;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setField2(String field2) {
|
||||
this.field2 = field2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.batch.extensions.notion.mapping.TestData.AllPropertiesSource;
|
||||
import org.springframework.batch.extensions.notion.mapping.TestData.PartialPropertiesSource;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.catchThrowable;
|
||||
import static org.assertj.core.api.BDDAssertions.from;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class ConstructorPropertyMapperTests {
|
||||
|
||||
@Nested
|
||||
class using_record_without_additional_constructors {
|
||||
|
||||
private record TestRecord(String field1, String field2) {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new ConstructorPropertyMapper<>(TestRecord.class);
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns("Value2", from(TestRecord::field2));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties_without_type_parameter(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new ConstructorPropertyMapper<>();
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns("Value2", from(TestRecord::field2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_fail_with_vararg_constructor_parameter() {
|
||||
// WHEN
|
||||
Throwable thrown = catchThrowable(() -> new ConstructorPropertyMapper<>(new TestRecord("value", "value")));
|
||||
// THEN
|
||||
then(thrown) //
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Please don't pass any values here. The type will be detected automagically.");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@PartialPropertiesSource
|
||||
void should_map_partial_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new ConstructorPropertyMapper<>(TestRecord.class);
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns(null, from(TestRecord::field2));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class using_record_with_additional_constructors {
|
||||
|
||||
private record TestRecord(String field1, String field2) {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private TestRecord() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private TestRecord(String field1) {
|
||||
this(field1, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_fail() {
|
||||
// WHEN
|
||||
Throwable thrown = catchThrowable(() -> new ConstructorPropertyMapper<>(TestRecord.class));
|
||||
// THEN
|
||||
then(thrown).isInstanceOf(IllegalArgumentException.class) //
|
||||
.cause() //
|
||||
.isInstanceOf(NoSuchMethodException.class)
|
||||
.hasMessageStartingWith("Multiple constructors available: ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.springframework.batch.extensions.notion.mapping.TestData.AllPropertiesSource;
|
||||
import org.springframework.batch.extensions.notion.mapping.TestData.PartialPropertiesSource;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.BDDAssertions.catchThrowable;
|
||||
import static org.assertj.core.api.BDDAssertions.from;
|
||||
import static org.assertj.core.api.BDDAssertions.then;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class RecordPropertyMapperTests {
|
||||
|
||||
@Nested
|
||||
class using_record_without_additional_constructors {
|
||||
|
||||
private record TestRecord(String field1, String field2) {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new RecordPropertyMapper<>(TestRecord.class);
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns("Value2", from(TestRecord::field2));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties_without_type_parameter(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new RecordPropertyMapper<>();
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns("Value2", from(TestRecord::field2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_fail_with_vararg_constructor_parameter() {
|
||||
// WHEN
|
||||
Throwable thrown = catchThrowable(() -> new RecordPropertyMapper<>(new TestRecord("value", "value")));
|
||||
// THEN
|
||||
then(thrown) //
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Please don't pass any values here. The type will be detected automagically.");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@PartialPropertiesSource
|
||||
void should_map_partial_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new RecordPropertyMapper<>(TestRecord.class);
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns(null, from(TestRecord::field2));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class using_record_with_additional_constructors {
|
||||
|
||||
private record TestRecord(String field1, String field2) {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private TestRecord() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private TestRecord(String field1) {
|
||||
this(field1, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@AllPropertiesSource
|
||||
void should_map_all_properties(Map<String, String> properties) {
|
||||
// GIVEN
|
||||
PropertyMapper<TestRecord> underTest = new RecordPropertyMapper<>(TestRecord.class);
|
||||
// WHEN
|
||||
TestRecord result = underTest.map(properties);
|
||||
// THEN
|
||||
then(result) //
|
||||
.returns("Value1", from(TestRecord::field1)) //
|
||||
.returns("Value2", from(TestRecord::field2));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or 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.
|
||||
*/
|
||||
package org.springframework.batch.extensions.notion.mapping;
|
||||
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* @author Stefano Cordio
|
||||
*/
|
||||
class TestData {
|
||||
|
||||
@Retention(RUNTIME)
|
||||
@MethodSource("org.springframework.batch.extensions.notion.mapping.TestData#all_properties")
|
||||
@interface AllPropertiesSource {
|
||||
|
||||
}
|
||||
|
||||
static Stream<Map<String, String>> all_properties() {
|
||||
return Stream.of(
|
||||
// FIXME not working with BeanWrapperPropertyMapper
|
||||
// Map.of("FIELD1", "Value1", "FIELD2", "Value2"), //
|
||||
Map.of("Field1", "Value1", "Field2", "Value2"), //
|
||||
Map.of("field1", "Value1", "field2", "Value2"));
|
||||
}
|
||||
|
||||
@Retention(RUNTIME)
|
||||
@MethodSource("org.springframework.batch.extensions.notion.mapping.TestData#partial_properties")
|
||||
@interface PartialPropertiesSource {
|
||||
|
||||
}
|
||||
|
||||
static Stream<Map<String, String>> partial_properties() {
|
||||
return Stream.of(
|
||||
// FIXME not working with BeanWrapperPropertyMapper
|
||||
// Map.of("FIELD1", "Value1"), //
|
||||
Map.of("Field1", "Value1"), //
|
||||
Map.of("field1", "Value1"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
junit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
|
||||
Reference in New Issue
Block a user