Skip to content

Commit bfc7a72

Browse files
committed
Replace notion-sdk-jvm with Spring Framework's HTTP Service Client
Signed-off-by: Stefano Cordio <[email protected]>
1 parent 2db69f0 commit bfc7a72

File tree

15 files changed

+480
-172
lines changed

15 files changed

+480
-172
lines changed

spring-batch-notion/pom.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,16 @@
6161
</dependency>
6262
<dependency>
6363
<groupId>org.springframework</groupId>
64-
<artifactId>spring-beans</artifactId>
64+
<artifactId>spring-web</artifactId>
6565
</dependency>
6666
<dependency>
6767
<groupId>org.springframework.batch</groupId>
6868
<artifactId>spring-batch-infrastructure</artifactId>
6969
</dependency>
70+
<dependency>
71+
<groupId>tools.jackson.core</groupId>
72+
<artifactId>jackson-databind</artifactId>
73+
</dependency>
7074
<!-- Test -->
7175
<dependency>
7276
<groupId>com.h2database</groupId>

spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,19 @@
1515
*/
1616
package org.springframework.batch.extensions.notion;
1717

18-
import notion.api.v1.NotionClient;
19-
import notion.api.v1.http.JavaNetHttpClient;
20-
import notion.api.v1.logging.Slf4jLogger;
21-
import notion.api.v1.model.databases.QueryResults;
22-
import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter;
23-
import notion.api.v1.model.databases.query.sort.QuerySort;
24-
import notion.api.v1.model.pages.Page;
25-
import notion.api.v1.model.pages.PageProperty;
26-
import notion.api.v1.model.pages.PageProperty.RichText;
27-
import notion.api.v1.request.databases.QueryDatabaseRequest;
2818
import org.jspecify.annotations.Nullable;
19+
import org.springframework.batch.extensions.notion.PageProperty.RichTextProperty;
20+
import org.springframework.batch.extensions.notion.PageProperty.TitleProperty;
2921
import org.springframework.batch.extensions.notion.mapping.PropertyMapper;
3022
import org.springframework.batch.infrastructure.item.ExecutionContext;
3123
import org.springframework.batch.infrastructure.item.ItemReader;
3224
import org.springframework.batch.infrastructure.item.data.AbstractPaginatedDataItemReader;
25+
import org.springframework.http.HttpHeaders;
3326
import org.springframework.util.Assert;
27+
import org.springframework.web.client.ApiVersionInserter;
28+
import org.springframework.web.client.RestClient;
29+
import org.springframework.web.client.support.RestClientAdapter;
30+
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
3431

3532
import java.util.Collections;
3633
import java.util.Iterator;
@@ -39,7 +36,6 @@
3936
import java.util.Map.Entry;
4037
import java.util.Objects;
4138
import java.util.stream.Collectors;
42-
import java.util.stream.Stream;
4339

4440
/**
4541
* Restartable {@link ItemReader} that reads entries from a Notion database via a paging
@@ -71,11 +67,11 @@ public class NotionDatabaseItemReader<T> extends AbstractPaginatedDataItemReader
7167

7268
private String baseUrl = DEFAULT_BASE_URL;
7369

74-
private @Nullable QueryTopLevelFilter filter;
70+
private @Nullable Filter filter;
7571

76-
private @Nullable List<QuerySort> sorts;
72+
private Sort[] sorts = new Sort[0];
7773

78-
private @Nullable NotionClient client;
74+
private @Nullable NotionDatabaseService service;
7975

8076
private boolean hasMore;
8177

@@ -117,7 +113,7 @@ public void setBaseUrl(String baseUrl) {
117113
* @see Filter#where(Filter)
118114
*/
119115
public void setFilter(Filter filter) {
120-
this.filter = filter.toQueryTopLevelFilter();
116+
this.filter = filter;
121117
}
122118

123119
/**
@@ -130,7 +126,7 @@ public void setFilter(Filter filter) {
130126
* @see Sort#by(Sort.Timestamp)
131127
*/
132128
public void setSorts(Sort... sorts) {
133-
this.sorts = Stream.of(sorts).map(Sort::toQuerySort).toList();
129+
this.sorts = sorts;
134130
}
135131

136132
/**
@@ -151,10 +147,15 @@ public void setPageSize(int pageSize) {
151147
*/
152148
@Override
153149
protected void doOpen() {
154-
client = new NotionClient(token);
155-
client.setHttpClient(new JavaNetHttpClient());
156-
client.setLogger(new Slf4jLogger());
157-
client.setBaseUrl(baseUrl);
150+
RestClient restClient = RestClient.builder()
151+
.baseUrl(baseUrl)
152+
.apiVersionInserter(ApiVersionInserter.useHeader("Notion-Version"))
153+
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
154+
.build();
155+
156+
RestClientAdapter adapter = RestClientAdapter.create(restClient);
157+
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
158+
service = factory.createClient(NotionDatabaseService.class);
158159

159160
hasMore = true;
160161
}
@@ -168,53 +169,47 @@ protected Iterator<T> doPageRead() {
168169
return Collections.emptyIterator();
169170
}
170171

171-
QueryDatabaseRequest request = new QueryDatabaseRequest(databaseId);
172-
request.setFilter(filter);
173-
request.setSorts(sorts);
174-
request.setStartCursor(nextCursor);
175-
request.setPageSize(pageSize);
172+
QueryRequest request = new QueryRequest(pageSize, nextCursor, filter, sorts);
176173

177174
@SuppressWarnings("DataFlowIssue")
178-
QueryResults queryResults = client.queryDatabase(request);
175+
QueryResult result = service.query(databaseId, request);
179176

180-
hasMore = queryResults.getHasMore();
181-
nextCursor = queryResults.getNextCursor();
177+
hasMore = result.hasMore();
178+
nextCursor = result.nextCursor();
182179

183-
return queryResults.getResults()
180+
return result.results()
184181
.stream()
185182
.map(NotionDatabaseItemReader::getProperties)
186183
.map(propertyMapper::map)
187184
.iterator();
188185
}
189186

190-
private static Map<String, String> getProperties(Page element) {
191-
return element.getProperties()
187+
private static Map<String, String> getProperties(Page page) {
188+
return page.properties()
192189
.entrySet()
193190
.stream()
194191
.collect(Collectors.toUnmodifiableMap(Entry::getKey, entry -> getPropertyValue(entry.getValue())));
195192
}
196193

197194
private static String getPropertyValue(PageProperty property) {
198-
return switch (property.getType()) {
199-
case RichText -> getPlainText(property.getRichText());
200-
case Title -> getPlainText(property.getTitle());
201-
default -> throw new IllegalArgumentException("Unsupported type: " + property.getType());
202-
};
195+
if (property instanceof RichTextProperty p) {
196+
return getPlainText(p.richText());
197+
}
198+
if (property instanceof TitleProperty p) {
199+
return getPlainText(p.title());
200+
}
201+
throw new IllegalArgumentException("Unsupported type: " + property.getClass());
203202
}
204203

205204
private static String getPlainText(List<RichText> texts) {
206-
return texts.isEmpty() ? "" : texts.get(0).getPlainText();
205+
return texts.isEmpty() ? "" : texts.get(0).plainText();
207206
}
208207

209208
/**
210209
* {@inheritDoc}
211210
*/
212-
@SuppressWarnings("DataFlowIssue")
213211
@Override
214212
protected void doClose() {
215-
client.close();
216-
client = null;
217-
218213
hasMore = false;
219214
}
220215

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.extensions.notion;
17+
18+
import org.springframework.http.MediaType;
19+
import org.springframework.web.bind.annotation.PathVariable;
20+
import org.springframework.web.bind.annotation.RequestBody;
21+
import org.springframework.web.service.annotation.HttpExchange;
22+
import org.springframework.web.service.annotation.PostExchange;
23+
24+
@HttpExchange(url = "/databases", version = "2022-06-28", accept = MediaType.APPLICATION_JSON_VALUE)
25+
interface NotionDatabaseService {
26+
27+
@PostExchange("/{databaseId}/query")
28+
QueryResult query(@PathVariable String databaseId, @RequestBody QueryRequest request);
29+
30+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.springframework.batch.extensions.notion;
2+
3+
import java.util.Map;
4+
5+
record Page(Map<String, PageProperty> properties) {
6+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.springframework.batch.extensions.notion;
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
5+
import com.fasterxml.jackson.annotation.JsonTypeName;
6+
import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
7+
import tools.jackson.databind.annotation.JsonNaming;
8+
9+
import java.util.List;
10+
11+
@JsonTypeInfo(use = Id.NAME, property = "type")
12+
interface PageProperty {
13+
14+
@JsonTypeName("rich_text")
15+
@JsonNaming(SnakeCaseStrategy.class)
16+
record RichTextProperty(List<RichText> richText) implements PageProperty {
17+
}
18+
19+
@JsonTypeName("title")
20+
record TitleProperty(List<RichText> title) implements PageProperty {
21+
}
22+
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.springframework.batch.extensions.notion;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
5+
import org.jspecify.annotations.Nullable;
6+
import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
7+
import tools.jackson.databind.annotation.JsonNaming;
8+
9+
import java.util.List;
10+
11+
@JsonNaming(SnakeCaseStrategy.class)
12+
@JsonInclude(Include.NON_EMPTY)
13+
record QueryRequest(int pageSize, @Nullable String startCursor, @Nullable Filter filter, Sort... sorts) {
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.springframework.batch.extensions.notion;
2+
3+
import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
4+
import tools.jackson.databind.annotation.JsonNaming;
5+
6+
import java.util.List;
7+
8+
@JsonNaming(SnakeCaseStrategy.class)
9+
record QueryResult(List<Page> results, String nextCursor, boolean hasMore) {
10+
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.springframework.batch.extensions.notion;
2+
3+
import tools.jackson.databind.PropertyNamingStrategies;
4+
import tools.jackson.databind.annotation.JsonNaming;
5+
6+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
7+
record RichText(String plainText) {
8+
}

spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Sort.java

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
*/
1616
package org.springframework.batch.extensions.notion;
1717

18-
import notion.api.v1.model.databases.query.sort.QuerySort;
19-
import notion.api.v1.model.databases.query.sort.QuerySortDirection;
20-
import notion.api.v1.model.databases.query.sort.QuerySortTimestamp;
18+
import com.fasterxml.jackson.annotation.JsonProperty;
19+
import tools.jackson.databind.EnumNamingStrategies;
20+
import tools.jackson.databind.EnumNamingStrategies.SnakeCaseStrategy;
21+
import tools.jackson.databind.annotation.EnumNaming;
2122

2223
import java.util.Objects;
2324

@@ -81,78 +82,55 @@ public static Sort by(Timestamp timestamp) {
8182
/**
8283
* Timestamps associated with database entries.
8384
*/
85+
@EnumNaming(SnakeCaseStrategy.class)
8486
public enum Timestamp {
8587

8688
/**
8789
* The time the entry was created.
8890
*/
89-
CREATED_TIME(QuerySortTimestamp.CreatedTime),
91+
CREATED_TIME,
9092

9193
/**
9294
* The time the entry was last edited.
9395
*/
94-
LAST_EDITED_TIME(QuerySortTimestamp.LastEditedTime);
95-
96-
private final QuerySortTimestamp querySortTimestamp;
97-
98-
Timestamp(QuerySortTimestamp querySortTimestamp) {
99-
this.querySortTimestamp = querySortTimestamp;
100-
}
101-
102-
private QuerySortTimestamp getQuerySortTimestamp() {
103-
return querySortTimestamp;
104-
}
96+
LAST_EDITED_TIME;
10597

10698
}
10799

108100
/**
109101
* Sort directions.
110102
*/
103+
@EnumNaming(SnakeCaseStrategy.class)
111104
public enum Direction {
112105

113106
/**
114107
* Ascending direction.
115108
*/
116-
ASCENDING(QuerySortDirection.Ascending),
109+
ASCENDING,
117110

118111
/**
119112
* Descending direction.
120113
*/
121-
DESCENDING(QuerySortDirection.Descending);
122-
123-
private final QuerySortDirection querySortDirection;
124-
125-
Direction(QuerySortDirection querySortDirection) {
126-
this.querySortDirection = querySortDirection;
127-
}
128-
129-
private QuerySortDirection getQuerySortDirection() {
130-
return querySortDirection;
131-
}
114+
DESCENDING;
132115

133116
}
134117

135118
private Sort() {
136119
}
137120

138-
abstract QuerySort toQuerySort();
139-
140121
private static final class PropertySort extends Sort {
141122

123+
@JsonProperty
142124
private final String property;
143125

126+
@JsonProperty
144127
private final Direction direction;
145128

146129
private PropertySort(String property, Direction direction) {
147130
this.property = Objects.requireNonNull(property);
148131
this.direction = Objects.requireNonNull(direction);
149132
}
150133

151-
@Override
152-
QuerySort toQuerySort() {
153-
return new QuerySort(property, null, direction.getQuerySortDirection());
154-
}
155-
156134
@Override
157135
public String toString() {
158136
return "%s: %s".formatted(property, direction);
@@ -162,20 +140,17 @@ public String toString() {
162140

163141
private static final class TimestampSort extends Sort {
164142

143+
@JsonProperty
165144
private final Timestamp timestamp;
166145

146+
@JsonProperty
167147
private final Direction direction;
168148

169149
private TimestampSort(Timestamp timestamp, Direction direction) {
170150
this.timestamp = Objects.requireNonNull(timestamp);
171151
this.direction = Objects.requireNonNull(direction);
172152
}
173153

174-
@Override
175-
QuerySort toQuerySort() {
176-
return new QuerySort(null, timestamp.getQuerySortTimestamp(), direction.getQuerySortDirection());
177-
}
178-
179154
@Override
180155
public String toString() {
181156
return "%s: %s".formatted(timestamp, direction);

0 commit comments

Comments
 (0)