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 33916b6..3f547fc 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; @@ -85,9 +91,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; } @@ -108,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) */ @@ -142,9 +151,6 @@ public void setPageSize(int pageSize) { super.setPageSize(pageSize); } - /** - * {@inheritDoc} - */ @Override protected void doOpen() { RestClient restClient = RestClient.builder() @@ -160,9 +166,6 @@ protected void doOpen() { hasMore = true; } - /** - * {@inheritDoc} - */ @Override protected Iterator doPageRead() { if (!hasMore) { @@ -205,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/builder/NotionDatabaseItemReaderBuilder.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilder.java new file mode 100644 index 0000000..6674831 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilder.java @@ -0,0 +1,234 @@ +/* + * 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.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; + +/** + * A builder for the {@link NotionDatabaseItemReader}. + * + * @author Jaeung Ha + * @param Type of item to be read + * @see NotionDatabaseItemReader + * @since 0.2.0 + */ +public class NotionDatabaseItemReaderBuilder { + + 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 = NotionDatabaseItemReader.DEFAULT_PAGE_SIZE; + + private boolean saveState = true; + + private int maxItemCount = Integer.MAX_VALUE; + + private int currentItemCount = 0; + + /** + * Create a new {@link NotionDatabaseItemReaderBuilder}. + */ + public NotionDatabaseItemReaderBuilder() { + } + + /** + * The Notion integration token. + * @param token the token + * @return the current instance of the builder + * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, + * PropertyMapper) + */ + public NotionDatabaseItemReaderBuilder token(String token) { + this.token = token; + return this; + } + + /** + * The UUID of the database to read from. + * @param databaseId the database UUID + * @return the current instance of the builder + * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, + * PropertyMapper) + */ + public NotionDatabaseItemReaderBuilder databaseId(String databaseId) { + this.databaseId = databaseId; + return this; + } + + /** + * The {@link PropertyMapper} responsible for mapping properties of a Notion item into + * a Java object. + * @param propertyMapper the property mapper + * @return the current instance of the builder + * @see NotionDatabaseItemReader#NotionDatabaseItemReader(String, String, + * PropertyMapper) + */ + public NotionDatabaseItemReaderBuilder propertyMapper(PropertyMapper propertyMapper) { + this.propertyMapper = propertyMapper; + return this; + } + + /** + * 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 the current instance of the builder + * @see NotionDatabaseItemReader#setBaseUrl(String) + */ + public NotionDatabaseItemReaderBuilder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * {@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 the current instance of the builder + * @see NotionDatabaseItemReader#setFilter(Filter) + */ + public NotionDatabaseItemReaderBuilder filter(Filter filter) { + this.filter = filter; + return this; + } + + /** + * {@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) { + this.sorts = sorts; + return this; + } + + /** + * The number of items to be read with each page. + *

+ * Defaults to {@value NotionDatabaseItemReader#DEFAULT_PAGE_SIZE}. + * @param pageSize the page size + * @return the current instance of the 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 the current instance of the builder + * @see NotionDatabaseItemReader#setSaveState(boolean) + */ + public NotionDatabaseItemReaderBuilder saveState(boolean saveState) { + this.saveState = saveState; + return this; + } + + /** + * 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) { + this.name = name; + return this; + } + + /** + * The maximum index of the items to be read. + * @param maxItemCount the maximum item count + * @return the current instance of the builder + * @see NotionDatabaseItemReader#setMaxItemCount(int) + */ + public NotionDatabaseItemReaderBuilder maxItemCount(int maxItemCount) { + this.maxItemCount = maxItemCount; + return this; + } + + /** + * The index of the item to start reading from. + * @param currentItemCount the current item count + * @return the current instance of the builder + * @see NotionDatabaseItemReader#setCurrentItemCount(int) + */ + public NotionDatabaseItemReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + return this; + } + + /** + * Builds the {@link NotionDatabaseItemReader}. + * @return the built reader + */ + public NotionDatabaseItemReader build() { + if (this.saveState && this.name != null) { + 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); + if (baseUrl != null) { + reader.setBaseUrl(baseUrl); + } + if (name != null) { + reader.setName(name); + } + if (filter != null) { + reader.setFilter(filter); + } + reader.setSorts(sorts); + reader.setPageSize(pageSize); + reader.setMaxItemCount(maxItemCount); + reader.setCurrentItemCount(currentItemCount); + + return reader; + } + +} 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 0000000..2083c3f --- /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 a76a140..445c80c 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 b8631e4..77611ab 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/builder/NotionDatabaseItemReaderBuilderTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/builder/NotionDatabaseItemReaderBuilderTests.java new file mode 100644 index 0000000..b75ba0f --- /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 0000000..2d26172 --- /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 3f7c60d..089385b 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 234c20b..590b4c0 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 c247950..ff0d4f1 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. */