From 16931143c77ece294d4277d2006e4a97dcc89144 Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Fri, 16 Jan 2026 11:02:22 +0900 Subject: [PATCH 1/7] Add NotionDatabaseItemReaderBuilder Signed-off-by: qlfyd123 --- .../NotionDatabaseItemReaderBuilder.java | 209 +++++++++++++++++ .../NotionDatabaseItemReaderBuilderTests.java | 210 ++++++++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java create mode 100644 spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java new file mode 100644 index 00000000..41a10225 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright 2024-2025 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.jspecify.annotations.Nullable; +import org.springframework.batch.extensions.notion.mapping.PropertyMapper; +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.util.Assert; + +/** + * A builder for the {@link NotionDatabaseItemReader}. + * + * @param Type of item to be read + * @author Jaeung Ha + * @see NotionDatabaseItemReader + */ +public class NotionDatabaseItemReaderBuilder { + + private static final int DEFAULT_PAGE_SIZE = 100; + + private @Nullable String token; + + private @Nullable String databaseId; + + private @Nullable PropertyMapper propertyMapper; + + private @Nullable String baseUrl; + + private @Nullable Filter filter; + + private @Nullable String name; + + private Sort[] sorts = new Sort[0]; + + private int pageSize = DEFAULT_PAGE_SIZE; + + private boolean saveState = true; + + private int maxItemCount = Integer.MAX_VALUE; + + private int currentItemCount = 0; + + /** + * Sets the Notion integration token. + * @param token the token + * @return this builder + * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, + * PropertyMapper) + */ + public NotionDatabaseItemReaderBuilder token(String token) { + this.token = token; + return this; + } + + /** + * Sets the UUID of the database to read from. + * @param databaseId the database UUID + * @return this builder + * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, + * PropertyMapper) + */ + public NotionDatabaseItemReaderBuilder databaseId(String databaseId) { + this.databaseId = databaseId; + return this; + } + + /** + * Sets the {@link PropertyMapper} to use. + * @param propertyMapper the property mapper + * @return this builder + * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, + * PropertyMapper) + */ + public NotionDatabaseItemReaderBuilder propertyMapper(PropertyMapper propertyMapper) { + this.propertyMapper = propertyMapper; + return this; + } + + /** + * Sets the base URL of the Notion API. + * @param baseUrl the base URL + * @return this builder + * @see NotionDatabaseItemReader#setBaseUrl(String) + */ + public NotionDatabaseItemReaderBuilder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Sets the {@link Filter} to apply. + * @param filter the filter + * @return this builder + * @see NotionDatabaseItemReader#setFilter(Filter) + */ + public NotionDatabaseItemReaderBuilder filter(Filter filter) { + this.filter = filter; + return this; + } + + /** + * Sets the {@link Sort}s to apply. + * @param sorts the sorts + * @return this builder + * @see NotionDatabaseItemReader#setSorts(Sort...) + */ + public NotionDatabaseItemReaderBuilder sorts(Sort... sorts) { + this.sorts = sorts; + return this; + } + + /** + * Sets the number of items to be read with each page. + * @param pageSize the page size + * @return this builder + * @see NotionDatabaseItemReader#setPageSize(int) + */ + public NotionDatabaseItemReaderBuilder pageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + /** + * Sets the flag that determines whether to save the state of the reader for restarts. + * @param saveState the save state flag + * @return this builder + * @see NotionDatabaseItemReader#setSaveState(boolean) + */ + public NotionDatabaseItemReaderBuilder saveState(boolean saveState) { + this.saveState = saveState; + return this; + } + + /** + * The name used to calculate the key within the {@link ExecutionContext}. Required if + * {@link #saveState(boolean)} is set to true.
+ * @param name the unique name of the component + * @return this builder + * @see NotionDatabaseItemReader#setName(String) + */ + public NotionDatabaseItemReaderBuilder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the maximum number of items to read. + * @param maxItemCount the maximum item count + * @return this builder + * @see NotionDatabaseItemReader#setMaxItemCount(int) + */ + public NotionDatabaseItemReaderBuilder maxItemCount(int maxItemCount) { + this.maxItemCount = maxItemCount; + return this; + } + + /** + * Sets the index of the item to start reading from. + * @param currentItemCount the current item count + * @return this builder + * @see NotionDatabaseItemReader#setCurrentItemCount(int) + */ + public NotionDatabaseItemReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + return this; + } + + /** + * Builds the {@link NotionDatabaseItemReader}. + * @return the built reader + * @throws IllegalArgumentException if required fields are missing + */ + public NotionDatabaseItemReader build() { + if (this.saveState) { + Assert.hasText(this.name, "A name is required when saveState is set to true."); + } + + NotionDatabaseItemReader reader = new NotionDatabaseItemReader<>(token, databaseId, propertyMapper); + + reader.setSaveState(saveState); + if (baseUrl != null) { + reader.setBaseUrl(baseUrl); + } + if (name != null) { + reader.setName(name); + } + reader.setFilter(filter); + reader.setSorts(sorts); + reader.setPageSize(pageSize); + reader.setMaxItemCount(maxItemCount); + reader.setCurrentItemCount(currentItemCount); + + return reader; + } + +} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java new file mode 100644 index 00000000..e83e8bc4 --- /dev/null +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2024-2025 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.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.batch.extensions.notion.mapping.PropertyMapper; +import org.springframework.test.util.ReflectionTestUtils; + +class NotionDatabaseItemReaderBuilderTests { + + @Test + void should_succeed() { + // GIVEN + String expectedToken = "FOO TOKEN"; + String expectedDatabaseId = "FOO DATABASE ID"; + PropertyMapper expectedPropertyMapper = properties -> "FOO PROPERTY"; + Filter expectedFilter = Filter.where().checkbox("IsActive").isEqualTo(true); + String expectedName = "FOO NAME"; + Sort[] expectedSorts = new Sort[0]; + int expectedPageSize = 50; + boolean expectedSaveState = true; + int expectedMaxItemCount = 1000; + int expectedCurrentItemCount = 10; + String expectedBaseUrl = "https://example.com"; + + // WHEN + NotionDatabaseItemReader reader = new NotionDatabaseItemReaderBuilder().token(expectedToken) + .databaseId(expectedDatabaseId) + .propertyMapper(expectedPropertyMapper) + .filter(expectedFilter) + .name(expectedName) + .sorts(expectedSorts) + .pageSize(expectedPageSize) + .saveState(expectedSaveState) + .maxItemCount(expectedMaxItemCount) + .currentItemCount(expectedCurrentItemCount) + .baseUrl(expectedBaseUrl) + .build(); + + // THEN + Assertions.assertThat(ReflectionTestUtils.getField(reader, "token")).isEqualTo(expectedToken); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "databaseId")).isEqualTo(expectedDatabaseId); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "propertyMapper")).isEqualTo(expectedPropertyMapper); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "filter")).isEqualTo(expectedFilter); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "sorts")).isEqualTo(expectedSorts); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "pageSize")).isEqualTo(expectedPageSize); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "saveState")).isEqualTo(expectedSaveState); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "name")).isEqualTo(expectedName); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "maxItemCount")).isEqualTo(expectedMaxItemCount); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "currentItemCount")) + .isEqualTo(expectedCurrentItemCount); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "baseUrl")).isEqualTo(expectedBaseUrl); + } + + @Test + void should_succeed_when_saveStateIsFalse_and_nameIsNull() { + // GIVEN + String token = "FOO TOKEN"; + String databaseId = "FOO DATABASE ID"; + PropertyMapper propertyMapper = properties -> "FOO PROPERTY"; + boolean saveState = false; + String name = null; + + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder().token(token) + .propertyMapper(propertyMapper) + .databaseId(databaseId) + .saveState(saveState) + .name(name); + + // WHEN + NotionDatabaseItemReader reader = builder.build(); + + // THEN + Assertions.assertThat(ReflectionTestUtils.getField(reader, "saveState")).isEqualTo(false); + Assertions.assertThat(ReflectionTestUtils.getField(reader, "name")).isNull(); + } + + @Test + void should_fail_when_tokenIsNull() { + // GIVEN + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token(null) + .databaseId("FOO DATABASE ID") + .propertyMapper(properties -> "FOO PROPERTY"); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build).isInstanceOf(NullPointerException.class); + } + + @Test + void should_fail_when_databaseIdIsNull() { + // GIVEN + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token("FOO TOKEN") + .databaseId(null) + .propertyMapper(properties -> "FOO PROPERTY"); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build).isInstanceOf(NullPointerException.class); + } + + @Test + void should_fail_when_propertyMapperIsNull() { + // GIVEN + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token("FOO TOKEN") + .databaseId("FOO DATABASE ID") + .propertyMapper(null); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build).isInstanceOf(NullPointerException.class); + } + + @Test + void should_fail_when_saveStateIsTrue_and_nameIsBlank() { + // GIVEN + String token = "FOO TOKEN"; + String databaseId = "FOO DATABASE ID"; + PropertyMapper propertyMapper = properties -> "FOO PROPERTY"; + boolean saveState = true; + String name = ""; + + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder().token(token) + .propertyMapper(propertyMapper) + .databaseId(databaseId) + .saveState(saveState) + .name(name); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A name is required when saveState is set to true."); + } + + @Test + void should_fail_when_saveStateIsTrue_and_nameIsNull() { + // GIVEN + String token = "FOO TOKEN"; + String databaseId = "FOO DATABASE ID"; + PropertyMapper propertyMapper = properties -> "FOO PROPERTY"; + boolean saveState = true; + String name = null; + + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder().token(token) + .propertyMapper(propertyMapper) + .databaseId(databaseId) + .saveState(saveState) + .name(name); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A name is required when saveState is set to true."); + } + + @Test + void should_fail_when_pageSizeIsGreaterThan100() { + // GIVEN + String token = "FOO TOKEN"; + String databaseId = "FOO DATABASE ID"; + PropertyMapper propertyMapper = properties -> "FOO PROPERTY"; + String name = "FOO NAME"; + int pageSize = 101; + + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder().token(token) + .propertyMapper(propertyMapper) + .databaseId(databaseId) + .name(name) + .pageSize(pageSize); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("pageSize must be less than or equal to 100"); + } + + @Test + void should_fail_when_pageSizeIsSmallerThanZero() { + // GIVEN + String token = "FOO TOKEN"; + String databaseId = "FOO DATABASE ID"; + PropertyMapper propertyMapper = properties -> "FOO PROPERTY"; + String name = "FOO NAME"; + int pageSize = -1; + + NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder().token(token) + .propertyMapper(propertyMapper) + .databaseId(databaseId) + .name(name) + .pageSize(pageSize); + + // WHEN & THEN + Assertions.assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("pageSize must be greater than zero"); + } + +} From 8d6eea261158c6f2340a2d4924862d11b7f4f27c Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Tue, 20 Jan 2026 11:17:39 +0900 Subject: [PATCH 2/7] add null check for arguments Signed-off-by: qlfyd123 --- .../extensions/notion/NotionDatabaseItemReaderBuilder.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java index 41a10225..4e508297 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java @@ -188,6 +188,9 @@ public NotionDatabaseItemReader build() { Assert.hasText(this.name, "A name is required when saveState is set to true."); } + if (token == null || databaseId == null || propertyMapper == null) { + throw new IllegalArgumentException("token, databaseId, and propertyMapper must not be null"); + } NotionDatabaseItemReader reader = new NotionDatabaseItemReader<>(token, databaseId, propertyMapper); reader.setSaveState(saveState); @@ -197,7 +200,9 @@ public NotionDatabaseItemReader build() { if (name != null) { reader.setName(name); } - reader.setFilter(filter); + if (filter != null) { + reader.setFilter(filter); + } reader.setSorts(sorts); reader.setPageSize(pageSize); reader.setMaxItemCount(maxItemCount); From 662c895897a28097e8fcfcb0ef11986e61d53184 Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Tue, 20 Jan 2026 11:18:26 +0900 Subject: [PATCH 3/7] alter Objects.requireNonNull methods to Assert.notNull method Signed-off-by: qlfyd123 --- .../extensions/notion/NotionDatabaseItemReader.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java index 04e0df37..39bbcb8d 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java @@ -85,9 +85,12 @@ public class NotionDatabaseItemReader extends AbstractPaginatedDataItemReader * of a Notion item into a Java object */ public NotionDatabaseItemReader(String token, String databaseId, PropertyMapper propertyMapper) { - this.token = Objects.requireNonNull(token); - this.databaseId = Objects.requireNonNull(databaseId); - this.propertyMapper = Objects.requireNonNull(propertyMapper); + Assert.notNull(token, "token is required"); + Assert.notNull(databaseId, "databaseId is required"); + Assert.notNull(propertyMapper, "propertyMapper is required"); + this.token = token; + this.databaseId = databaseId; + this.propertyMapper = propertyMapper; this.pageSize = DEFAULT_PAGE_SIZE; } From 0ed0a86ad8f22cb00ee9314b27aad1e5cd4d2e78 Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Tue, 20 Jan 2026 14:44:01 +0900 Subject: [PATCH 4/7] Accept the reviewed changes and fix the incorrect test code. Signed-off-by: qlfyd123 --- .../NotionDatabaseItemReaderBuilderTests.java | 142 ++++++++++-------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java index e83e8bc4..812f3af7 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java @@ -16,55 +16,55 @@ package org.springframework.batch.extensions.notion; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.batch.extensions.notion.mapping.PropertyMapper; -import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.BDDAssertions.catchThrowable; +import static org.assertj.core.api.BDDAssertions.then; class NotionDatabaseItemReaderBuilderTests { @Test void should_succeed() { // GIVEN - String expectedToken = "FOO TOKEN"; - String expectedDatabaseId = "FOO DATABASE ID"; - PropertyMapper expectedPropertyMapper = properties -> "FOO PROPERTY"; - Filter expectedFilter = Filter.where().checkbox("IsActive").isEqualTo(true); - String expectedName = "FOO NAME"; - Sort[] expectedSorts = new Sort[0]; - int expectedPageSize = 50; - boolean expectedSaveState = true; - int expectedMaxItemCount = 1000; - int expectedCurrentItemCount = 10; - String expectedBaseUrl = "https://example.com"; + String token = "FOO TOKEN"; + String databaseId = "FOO DATABASE ID"; + PropertyMapper propertyMapper = properties -> "FOO PROPERTY"; + Filter filter = Filter.where().checkbox("IsActive").isEqualTo(true); + String name = "FOO NAME"; + Sort[] sorts = new Sort[0]; + int pageSize = 50; + boolean saveState = true; + int maxItemCount = 1000; + int currentItemCount = 10; + String baseUrl = "https://example.com"; // WHEN - NotionDatabaseItemReader reader = new NotionDatabaseItemReaderBuilder().token(expectedToken) - .databaseId(expectedDatabaseId) - .propertyMapper(expectedPropertyMapper) - .filter(expectedFilter) - .name(expectedName) - .sorts(expectedSorts) - .pageSize(expectedPageSize) - .saveState(expectedSaveState) - .maxItemCount(expectedMaxItemCount) - .currentItemCount(expectedCurrentItemCount) - .baseUrl(expectedBaseUrl) + NotionDatabaseItemReader reader = new NotionDatabaseItemReaderBuilder().token(token) + .databaseId(databaseId) + .propertyMapper(propertyMapper) + .filter(filter) + .name(name) + .sorts(sorts) + .pageSize(pageSize) + .saveState(saveState) + .maxItemCount(maxItemCount) + .currentItemCount(currentItemCount) + .baseUrl(baseUrl) .build(); // THEN - Assertions.assertThat(ReflectionTestUtils.getField(reader, "token")).isEqualTo(expectedToken); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "databaseId")).isEqualTo(expectedDatabaseId); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "propertyMapper")).isEqualTo(expectedPropertyMapper); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "filter")).isEqualTo(expectedFilter); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "sorts")).isEqualTo(expectedSorts); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "pageSize")).isEqualTo(expectedPageSize); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "saveState")).isEqualTo(expectedSaveState); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "name")).isEqualTo(expectedName); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "maxItemCount")).isEqualTo(expectedMaxItemCount); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "currentItemCount")) - .isEqualTo(expectedCurrentItemCount); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "baseUrl")).isEqualTo(expectedBaseUrl); + then(reader).extracting("token").isEqualTo(token); + then(reader).extracting("databaseId").isEqualTo(databaseId); + then(reader).extracting("propertyMapper").isEqualTo(propertyMapper); + then(reader).extracting("filter").isEqualTo(filter); + then(reader).extracting("sorts").isEqualTo(sorts); + then(reader).extracting("pageSize").isEqualTo(pageSize); + then(reader).extracting("saveState").isEqualTo(saveState); + then(reader).extracting("name").isEqualTo(name); + then(reader).extracting("maxItemCount").isEqualTo(maxItemCount); + then(reader).extracting("currentItemCount").isEqualTo(currentItemCount); + then(reader).extracting("baseUrl").isEqualTo(baseUrl); } @Test @@ -86,8 +86,8 @@ void should_succeed_when_saveStateIsFalse_and_nameIsNull() { NotionDatabaseItemReader reader = builder.build(); // THEN - Assertions.assertThat(ReflectionTestUtils.getField(reader, "saveState")).isEqualTo(false); - Assertions.assertThat(ReflectionTestUtils.getField(reader, "name")).isNull(); + then(reader).extracting("saveState").isEqualTo(false); + then(reader).extracting("name").isEqualTo(NotionDatabaseItemReader.class.getSimpleName()); } @Test @@ -95,10 +95,15 @@ void should_fail_when_tokenIsNull() { // GIVEN NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token(null) .databaseId("FOO DATABASE ID") - .propertyMapper(properties -> "FOO PROPERTY"); + .propertyMapper(properties -> "FOO PROPERTY") + .saveState(false); + + // WHEN + Throwable exception = catchThrowable(builder::build); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build).isInstanceOf(NullPointerException.class); + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class); + then(exception).hasMessage("token, databaseId, and propertyMapper must not be null"); } @Test @@ -106,10 +111,15 @@ void should_fail_when_databaseIdIsNull() { // GIVEN NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token("FOO TOKEN") .databaseId(null) - .propertyMapper(properties -> "FOO PROPERTY"); + .propertyMapper(properties -> "FOO PROPERTY") + .saveState(false); + + // WHEN + Throwable exception = catchThrowable(builder::build); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build).isInstanceOf(NullPointerException.class); + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class); + then(exception).hasMessage("token, databaseId, and propertyMapper must not be null"); } @Test @@ -117,10 +127,15 @@ void should_fail_when_propertyMapperIsNull() { // GIVEN NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token("FOO TOKEN") .databaseId("FOO DATABASE ID") - .propertyMapper(null); + .propertyMapper(null) + .saveState(false); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build).isInstanceOf(NullPointerException.class); + // WHEN + Throwable exception = catchThrowable(builder::build); + + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class); + then(exception).hasMessage("token, databaseId, and propertyMapper must not be null"); } @Test @@ -138,9 +153,11 @@ void should_fail_when_saveStateIsTrue_and_nameIsBlank() { .saveState(saveState) .name(name); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build) - .isInstanceOf(IllegalArgumentException.class) + // WHEN + Throwable exception = catchThrowable(builder::build); + + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class) .hasMessage("A name is required when saveState is set to true."); } @@ -159,9 +176,11 @@ void should_fail_when_saveStateIsTrue_and_nameIsNull() { .saveState(saveState) .name(name); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build) - .isInstanceOf(IllegalArgumentException.class) + // WHEN + Throwable exception = catchThrowable(builder::build); + + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class) .hasMessage("A name is required when saveState is set to true."); } @@ -180,9 +199,11 @@ void should_fail_when_pageSizeIsGreaterThan100() { .name(name) .pageSize(pageSize); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build) - .isInstanceOf(IllegalArgumentException.class) + // WHEN + Throwable exception = catchThrowable(builder::build); + + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class) .hasMessage("pageSize must be less than or equal to 100"); } @@ -201,10 +222,11 @@ void should_fail_when_pageSizeIsSmallerThanZero() { .name(name) .pageSize(pageSize); - // WHEN & THEN - Assertions.assertThatThrownBy(builder::build) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("pageSize must be greater than zero"); + // WHEN + Throwable exception = catchThrowable(builder::build); + + // THEN + then(exception).isInstanceOf(IllegalArgumentException.class).hasMessage("pageSize must be greater than zero"); } } From 30748dec6c42addf4e36e7b2d570cf8d5b37ffc4 Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Tue, 20 Jan 2026 17:12:53 +0900 Subject: [PATCH 5/7] update README.md Signed-off-by: qlfyd123 --- spring-batch-notion/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-batch-notion/README.md b/spring-batch-notion/README.md index 6be1b043..be0f48ac 100644 --- a/spring-batch-notion/README.md +++ b/spring-batch-notion/README.md @@ -53,12 +53,12 @@ The following constructor parameters should be provided: and 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). | -| `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. | -| `sorts` | no | `null` | `Sort` conditions to order the returned items. Each condition is applied following the declaration order. | +| 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). | +| `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. | +| `sorts` | no | `Empty Sort Array(new Sort[0])` | `Sort` conditions to order the returned items. Each condition is applied following the declaration order. | 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) From fb9b306ca282f122a235254d29452d7e7298ad4a Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Tue, 20 Jan 2026 17:33:59 +0900 Subject: [PATCH 6/7] Suppress NullAway in test scope Signed-off-by: qlfyd123 --- .../extensions/notion/NotionDatabaseItemReaderBuilderTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java index 812f3af7..6e72992a 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java @@ -22,6 +22,7 @@ import static org.assertj.core.api.BDDAssertions.catchThrowable; import static org.assertj.core.api.BDDAssertions.then; +@SuppressWarnings({ "DataFlowIssue", "null", "NullAway" }) class NotionDatabaseItemReaderBuilderTests { @Test From 7ad6584668253d05d71bbbe7b36a8fb740c053d2 Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Tue, 20 Jan 2026 17:44:59 +0900 Subject: [PATCH 7/7] fix incorrect Javadoc Signed-off-by: qlfyd123 --- .../notion/NotionDatabaseItemReaderBuilder.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java index 4e508297..f6373fe3 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java @@ -53,6 +53,12 @@ public class NotionDatabaseItemReaderBuilder { private int currentItemCount = 0; + /** + * Default constructor for {@link NotionDatabaseItemReaderBuilder}. + */ + public NotionDatabaseItemReaderBuilder() { + } + /** * Sets the Notion integration token. * @param token the token @@ -146,7 +152,7 @@ public NotionDatabaseItemReaderBuilder saveState(boolean saveState) { /** * The name used to calculate the key within the {@link ExecutionContext}. Required if - * {@link #saveState(boolean)} is set to true.
+ * {@link #saveState(boolean)} is set to true.
* @param name the unique name of the component * @return this builder * @see NotionDatabaseItemReader#setName(String)