From 16931143c77ece294d4277d2006e4a97dcc89144 Mon Sep 17 00:00:00 2001 From: qlfyd123 Date: Fri, 16 Jan 2026 11:02:22 +0900 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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) From 0d2b203ee436505b7537571c18cb70d9a29e65bf Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 3 Mar 2026 00:15:08 +0100 Subject: [PATCH 8/9] Update license headers Signed-off-by: Stefano Cordio --- .../extensions/notion/NotionDatabaseItemReaderBuilder.java | 2 +- .../notion/NotionDatabaseItemReaderBuilderTests.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 f6373fe3..4becdcce 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 @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 the original author or authors. + * Copyright 2024-2026 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. 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 6e72992a..786f69ab 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 @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 the original author or authors. + * Copyright 2024-2026 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.batch.extensions.notion; import org.junit.jupiter.api.Test; From 69b5d9aac771559be836733d4367f7bf5a7ccae6 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 3 Mar 2026 01:29:56 +0100 Subject: [PATCH 9/9] Polish contribution Signed-off-by: Stefano Cordio --- .../notion/NotionDatabaseItemReader.java | 26 +- .../NotionDatabaseItemReaderBuilder.java | 78 +++--- .../notion/builder/package-info.java | 23 ++ .../notion/mapping/package-info.java | 1 + .../batch/extensions/notion/package-info.java | 1 + .../NotionDatabaseItemReaderBuilderTests.java | 232 ------------------ .../NotionDatabaseItemReaderBuilderTests.java | 202 +++++++++++++++ .../notion/builder/package-info.java | 20 ++ .../extensions/notion/it/package-info.java | 1 + .../notion/it/pagination/package-info.java | 1 + .../batch/extensions/notion/package-info.java | 1 + 11 files changed, 306 insertions(+), 280 deletions(-) rename spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/{ => builder}/NotionDatabaseItemReaderBuilder.java (70%) create mode 100644 spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/package-info.java delete mode 100644 spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java create mode 100644 spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilderTests.java create mode 100644 spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/package-info.java 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 c2ea0e8c..3f547fc3 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 @@ -47,7 +47,7 @@ * the reader will begin again at the same number item it left off at. *

* This implementation is thread-safe between calls to {@link #open(ExecutionContext)}, - * but remember to set saveState to false if used in a + * but remember to {@link #setSaveState(boolean)} to {@code false} if used in a * multi-threaded environment (no restart available). * * @author Stefano Cordio @@ -55,9 +55,15 @@ */ public class NotionDatabaseItemReader extends AbstractPaginatedDataItemReader { - private static final int DEFAULT_PAGE_SIZE = 100; + /** + * Default number of items to be read with each page. + */ + public static final int DEFAULT_PAGE_SIZE = 100; - private static final String DEFAULT_BASE_URL = "https://api.notion.com/v1"; + /** + * Default URL of the Notion API. + */ + public static final String DEFAULT_BASE_URL = "https://api.notion.com/v1"; private final String token; @@ -111,7 +117,7 @@ public void setBaseUrl(String baseUrl) { * {@link Filter} condition to limit the returned items. *

* If no filter is provided, all the items in the database will be returned. - * @param filter the {@link Filter} conditions + * @param filter the {@link Filter} condition * @see Filter#where() * @see Filter#where(Filter) */ @@ -145,9 +151,6 @@ public void setPageSize(int pageSize) { super.setPageSize(pageSize); } - /** - * {@inheritDoc} - */ @Override protected void doOpen() { RestClient restClient = RestClient.builder() @@ -163,9 +166,6 @@ protected void doOpen() { hasMore = true; } - /** - * {@inheritDoc} - */ @Override protected Iterator doPageRead() { if (!hasMore) { @@ -208,17 +208,11 @@ private static String getPlainText(List texts) { return texts.isEmpty() ? "" : texts.get(0).plainText(); } - /** - * {@inheritDoc} - */ @Override protected void doClose() { hasMore = false; } - /** - * {@inheritDoc} - */ @Override protected void jumpToItem(int itemIndex) throws Exception { for (int i = 0; i < itemIndex; i++) { 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/builder/NotionDatabaseItemReaderBuilder.java similarity index 70% rename from spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilder.java rename to spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilder.java index 4becdcce..66748310 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/builder/NotionDatabaseItemReaderBuilder.java @@ -13,9 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.batch.extensions.notion; +package org.springframework.batch.extensions.notion.builder; import org.jspecify.annotations.Nullable; +import org.springframework.batch.extensions.notion.Filter; +import org.springframework.batch.extensions.notion.NotionDatabaseItemReader; +import org.springframework.batch.extensions.notion.Sort; import org.springframework.batch.extensions.notion.mapping.PropertyMapper; import org.springframework.batch.infrastructure.item.ExecutionContext; import org.springframework.util.Assert; @@ -23,14 +26,13 @@ /** * A builder for the {@link NotionDatabaseItemReader}. * - * @param Type of item to be read * @author Jaeung Ha + * @param Type of item to be read * @see NotionDatabaseItemReader + * @since 0.2.0 */ public class NotionDatabaseItemReaderBuilder { - private static final int DEFAULT_PAGE_SIZE = 100; - private @Nullable String token; private @Nullable String databaseId; @@ -45,7 +47,7 @@ public class NotionDatabaseItemReaderBuilder { private Sort[] sorts = new Sort[0]; - private int pageSize = DEFAULT_PAGE_SIZE; + private int pageSize = NotionDatabaseItemReader.DEFAULT_PAGE_SIZE; private boolean saveState = true; @@ -54,15 +56,15 @@ public class NotionDatabaseItemReaderBuilder { private int currentItemCount = 0; /** - * Default constructor for {@link NotionDatabaseItemReaderBuilder}. + * Create a new {@link NotionDatabaseItemReaderBuilder}. */ public NotionDatabaseItemReaderBuilder() { } /** - * Sets the Notion integration token. + * The Notion integration token. * @param token the token - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, * PropertyMapper) */ @@ -72,9 +74,9 @@ public NotionDatabaseItemReaderBuilder token(String token) { } /** - * Sets the UUID of the database to read from. + * The UUID of the database to read from. * @param databaseId the database UUID - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, * PropertyMapper) */ @@ -84,9 +86,10 @@ public NotionDatabaseItemReaderBuilder databaseId(String databaseId) { } /** - * Sets the {@link PropertyMapper} to use. + * The {@link PropertyMapper} responsible for mapping properties of a Notion item into + * a Java object. * @param propertyMapper the property mapper - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, * PropertyMapper) */ @@ -96,9 +99,14 @@ public NotionDatabaseItemReaderBuilder propertyMapper(PropertyMapper prope } /** - * Sets the base URL of the Notion API. + * The base URL of the Notion API. + *

+ * Defaults to {@value NotionDatabaseItemReader#DEFAULT_BASE_URL}. + *

+ * A custom value can be provided for testing purposes (e.g., the URL of a WireMock + * server). * @param baseUrl the base URL - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#setBaseUrl(String) */ public NotionDatabaseItemReaderBuilder baseUrl(String baseUrl) { @@ -107,9 +115,11 @@ public NotionDatabaseItemReaderBuilder baseUrl(String baseUrl) { } /** - * Sets the {@link Filter} to apply. + * {@link Filter} condition to limit the returned items. + *

+ * If no filter is provided, all the items in the database will be returned. * @param filter the filter - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#setFilter(Filter) */ public NotionDatabaseItemReaderBuilder filter(Filter filter) { @@ -118,9 +128,12 @@ public NotionDatabaseItemReaderBuilder filter(Filter filter) { } /** - * Sets the {@link Sort}s to apply. - * @param sorts the sorts - * @return this builder + * {@link Sort} conditions to order the returned items. + *

+ * Each condition is applied following the declaration order, i.e., earlier sorts take + * precedence over later ones. + * @param sorts the {@link Sort} conditions + * @return the current instance of the builder * @see NotionDatabaseItemReader#setSorts(Sort...) */ public NotionDatabaseItemReaderBuilder sorts(Sort... sorts) { @@ -129,9 +142,11 @@ public NotionDatabaseItemReaderBuilder sorts(Sort... sorts) { } /** - * Sets the number of items to be read with each page. + * The number of items to be read with each page. + *

+ * Defaults to {@value NotionDatabaseItemReader#DEFAULT_PAGE_SIZE}. * @param pageSize the page size - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#setPageSize(int) */ public NotionDatabaseItemReaderBuilder pageSize(int pageSize) { @@ -142,7 +157,7 @@ public NotionDatabaseItemReaderBuilder pageSize(int pageSize) { /** * Sets the flag that determines whether to save the state of the reader for restarts. * @param saveState the save state flag - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#setSaveState(boolean) */ public NotionDatabaseItemReaderBuilder saveState(boolean saveState) { @@ -151,10 +166,10 @@ 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.
- * @param name the unique name of the component - * @return this builder + * The name of the component which will be used as a stem for keys in the + * {@link ExecutionContext}. + * @param name the name for the component + * @return the current instance of the builder * @see NotionDatabaseItemReader#setName(String) */ public NotionDatabaseItemReaderBuilder name(String name) { @@ -163,9 +178,9 @@ public NotionDatabaseItemReaderBuilder name(String name) { } /** - * Sets the maximum number of items to read. + * The maximum index of the items to be read. * @param maxItemCount the maximum item count - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#setMaxItemCount(int) */ public NotionDatabaseItemReaderBuilder maxItemCount(int maxItemCount) { @@ -174,9 +189,9 @@ public NotionDatabaseItemReaderBuilder maxItemCount(int maxItemCount) { } /** - * Sets the index of the item to start reading from. + * The index of the item to start reading from. * @param currentItemCount the current item count - * @return this builder + * @return the current instance of the builder * @see NotionDatabaseItemReader#setCurrentItemCount(int) */ public NotionDatabaseItemReaderBuilder currentItemCount(int currentItemCount) { @@ -187,10 +202,9 @@ public NotionDatabaseItemReaderBuilder currentItemCount(int currentItemCount) /** * Builds the {@link NotionDatabaseItemReader}. * @return the built reader - * @throws IllegalArgumentException if required fields are missing */ public NotionDatabaseItemReader build() { - if (this.saveState) { + if (this.saveState && this.name != null) { Assert.hasText(this.name, "A name is required when saveState is set to true."); } diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/package-info.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/package-info.java new file mode 100644 index 00000000..2083c3f5 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024-2026 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. + */ + +/** + * Builder for Notion item reader. + */ +@NullMarked +package org.springframework.batch.extensions.notion.builder; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/mapping/package-info.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/mapping/package-info.java index a76a140c..445c80c7 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/mapping/package-info.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/mapping/package-info.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** * Infrastructure to map the properties of a Notion item into a Java object. */ diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/package-info.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/package-info.java index b8631e49..77611abf 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/package-info.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/package-info.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** * Spring Batch extension for Notion. */ 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 deleted file mode 100644 index 786f69ab..00000000 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReaderBuilderTests.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2024-2026 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.junit.jupiter.api.Test; -import org.springframework.batch.extensions.notion.mapping.PropertyMapper; - -import static org.assertj.core.api.BDDAssertions.catchThrowable; -import static org.assertj.core.api.BDDAssertions.then; - -@SuppressWarnings({ "DataFlowIssue", "null", "NullAway" }) -class NotionDatabaseItemReaderBuilderTests { - - @Test - void should_succeed() { - // GIVEN - 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(token) - .databaseId(databaseId) - .propertyMapper(propertyMapper) - .filter(filter) - .name(name) - .sorts(sorts) - .pageSize(pageSize) - .saveState(saveState) - .maxItemCount(maxItemCount) - .currentItemCount(currentItemCount) - .baseUrl(baseUrl) - .build(); - - // THEN - 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 - 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 - then(reader).extracting("saveState").isEqualTo(false); - then(reader).extracting("name").isEqualTo(NotionDatabaseItemReader.class.getSimpleName()); - } - - @Test - void should_fail_when_tokenIsNull() { - // GIVEN - NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token(null) - .databaseId("FOO DATABASE ID") - .propertyMapper(properties -> "FOO PROPERTY") - .saveState(false); - - // WHEN - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).isInstanceOf(IllegalArgumentException.class); - then(exception).hasMessage("token, databaseId, and propertyMapper must not be null"); - } - - @Test - void should_fail_when_databaseIdIsNull() { - // GIVEN - NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token("FOO TOKEN") - .databaseId(null) - .propertyMapper(properties -> "FOO PROPERTY") - .saveState(false); - - // WHEN - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).isInstanceOf(IllegalArgumentException.class); - then(exception).hasMessage("token, databaseId, and propertyMapper must not be null"); - } - - @Test - void should_fail_when_propertyMapperIsNull() { - // GIVEN - NotionDatabaseItemReaderBuilder builder = new NotionDatabaseItemReaderBuilder<>().token("FOO TOKEN") - .databaseId("FOO DATABASE ID") - .propertyMapper(null) - .saveState(false); - - // WHEN - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).isInstanceOf(IllegalArgumentException.class); - then(exception).hasMessage("token, databaseId, and propertyMapper must not be null"); - } - - @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 - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).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 - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).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 - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).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 - Throwable exception = catchThrowable(builder::build); - - // THEN - then(exception).isInstanceOf(IllegalArgumentException.class).hasMessage("pageSize must be greater than zero"); - } - -} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilderTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilderTests.java new file mode 100644 index 00000000..b75ba0f3 --- /dev/null +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilderTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2024-2026 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.builder; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.extensions.notion.Filter; +import org.springframework.batch.extensions.notion.NotionDatabaseItemReader; +import org.springframework.batch.extensions.notion.Sort; +import org.springframework.batch.extensions.notion.mapping.PropertyMapper; + +import static org.assertj.core.api.BDDAssertions.catchException; +import static org.assertj.core.api.BDDAssertions.then; +import static org.springframework.util.ClassUtils.getShortName; + +/** + * @author Jaeung Ha + */ +class NotionDatabaseItemReaderBuilderTests { + + @Test + void should_succeed() { + // GIVEN + String token = "TOKEN"; + String databaseId = "DATABASE ID"; + PropertyMapper propertyMapper = properties -> "PROPERTY"; + Filter filter = Filter.where().checkbox("IsActive").isEqualTo(true); + String name = "NAME"; + Sort[] sorts = new Sort[0]; + int pageSize = 50; + boolean saveState = true; + int maxItemCount = 1000; + int currentItemCount = 10; + String baseUrl = "https://example.com"; + + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder() // + .token(token) + .databaseId(databaseId) + .propertyMapper(propertyMapper) + .filter(filter) + .name(name) + .sorts(sorts) + .pageSize(pageSize) + .saveState(saveState) + .maxItemCount(maxItemCount) + .currentItemCount(currentItemCount) + .baseUrl(baseUrl); + + // WHEN + NotionDatabaseItemReader reader = underTest.build(); + + // THEN + 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 + void should_fail_when_token_is_null() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder<>() // + .token(null) + .databaseId("DATABASE ID") + .propertyMapper(properties -> "PROPERTY") + .saveState(false); + + // WHEN + Exception exception = catchException(underTest::build); + + // THEN + then(exception) // + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("token, databaseId, and propertyMapper must not be null"); + } + + @Test + void should_fail_when_databaseId_is_null() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder<>().token("TOKEN") + .databaseId(null) + .propertyMapper(properties -> "PROPERTY") + .saveState(false); + + // WHEN + Exception exception = catchException(underTest::build); + + // THEN + then(exception) // + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("token, databaseId, and propertyMapper must not be null"); + } + + @Test + void should_fail_when_propertyMapper_is_null() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder<>().token("TOKEN") + .databaseId("DATABASE ID") + .propertyMapper(null) + .saveState(false); + + // WHEN + Exception exception = catchException(underTest::build); + + // THEN + then(exception) // + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("token, databaseId, and propertyMapper must not be null"); + } + + @Test + void should_succeed_when_saveState_is_false_and_name_is_not_set() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder() // + .token("TOKEN") + .propertyMapper(properties -> "PROPERTY") + .databaseId("DATABASE ID") + .saveState(false); + + // WHEN + NotionDatabaseItemReader reader = underTest.build(); + + // THEN + then(reader).extracting("saveState").isEqualTo(false); + then(reader).extracting("name").isEqualTo(getShortName(NotionDatabaseItemReader.class)); + } + + @Test + void should_fail_when_saveState_is_true_and_name_is_blank() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder() // + .token("TOKEN") + .propertyMapper(properties -> "PROPERTY") + .databaseId("DATABASE ID") + .saveState(true) + .name(""); + + // WHEN + Exception exception = catchException(underTest::build); + + // THEN + then(exception) // + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A name is required when saveState is set to true."); + } + + @Test + void should_fail_when_pageSize_is_greater_than_100() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder() // + .token("TOKEN") + .propertyMapper(properties -> "PROPERTY") + .databaseId("DATABASE ID") + .pageSize(101); + + // WHEN + Exception exception = catchException(underTest::build); + + // THEN + then(exception) // + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("pageSize must be less than or equal to 100"); + } + + @Test + void should_fail_when_pageSize_is_less_than_zero() { + // GIVEN + NotionDatabaseItemReaderBuilder underTest = new NotionDatabaseItemReaderBuilder() // + .token("TOKEN") + .propertyMapper(properties -> "PROPERTY") + .databaseId("DATABASE ID") + .pageSize(-1); + + // WHEN + Exception exception = catchException(underTest::build); + + // THEN + then(exception) // + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("pageSize must be greater than zero"); + } + +} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/package-info.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/package-info.java new file mode 100644 index 00000000..2d26172c --- /dev/null +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2024-2026 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. + */ + +@NullUnmarked +package org.springframework.batch.extensions.notion.builder; + +import org.jspecify.annotations.NullUnmarked; diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/package-info.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/package-info.java index 3f7c60d3..089385b1 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/package-info.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/package-info.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + @NullUnmarked package org.springframework.batch.extensions.notion.it; diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/pagination/package-info.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/pagination/package-info.java index 234c20b2..590b4c01 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/pagination/package-info.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/it/pagination/package-info.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + @NullUnmarked package org.springframework.batch.extensions.notion.it.pagination; diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/package-info.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/package-info.java index c2479504..ff0d4f17 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/package-info.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/package-info.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** * Tests for Spring Batch Notion. */