Skip to content

Commit 9dd7a40

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 9dd7a40

File tree

11 files changed

+438
-172
lines changed

11 files changed

+438
-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: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@
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.QueryResult.Page;
20+
import org.springframework.batch.extensions.notion.QueryResult.Page.PageProperty;
21+
import org.springframework.batch.extensions.notion.QueryResult.Page.PageProperty.RichText;
22+
import org.springframework.batch.extensions.notion.QueryResult.Page.PageProperty.RichTextProperty;
23+
import org.springframework.batch.extensions.notion.QueryResult.Page.PageProperty.TitleProperty;
2924
import org.springframework.batch.extensions.notion.mapping.PropertyMapper;
3025
import org.springframework.batch.infrastructure.item.ExecutionContext;
3126
import org.springframework.batch.infrastructure.item.ItemReader;
3227
import org.springframework.batch.infrastructure.item.data.AbstractPaginatedDataItemReader;
28+
import org.springframework.http.HttpHeaders;
3329
import org.springframework.util.Assert;
30+
import org.springframework.web.client.ApiVersionInserter;
31+
import org.springframework.web.client.RestClient;
32+
import org.springframework.web.client.support.RestClientAdapter;
33+
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
3434

3535
import java.util.Collections;
3636
import java.util.Iterator;
@@ -39,7 +39,6 @@
3939
import java.util.Map.Entry;
4040
import java.util.Objects;
4141
import java.util.stream.Collectors;
42-
import java.util.stream.Stream;
4342

4443
/**
4544
* Restartable {@link ItemReader} that reads entries from a Notion database via a paging
@@ -71,11 +70,11 @@ public class NotionDatabaseItemReader<T> extends AbstractPaginatedDataItemReader
7170

7271
private String baseUrl = DEFAULT_BASE_URL;
7372

74-
private @Nullable QueryTopLevelFilter filter;
73+
private @Nullable Filter filter;
7574

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

78-
private @Nullable NotionClient client;
77+
private @Nullable NotionDatabaseService service;
7978

8079
private boolean hasMore;
8180

@@ -117,7 +116,7 @@ public void setBaseUrl(String baseUrl) {
117116
* @see Filter#where(Filter)
118117
*/
119118
public void setFilter(Filter filter) {
120-
this.filter = filter.toQueryTopLevelFilter();
119+
this.filter = filter;
121120
}
122121

123122
/**
@@ -130,7 +129,7 @@ public void setFilter(Filter filter) {
130129
* @see Sort#by(Sort.Timestamp)
131130
*/
132131
public void setSorts(Sort... sorts) {
133-
this.sorts = Stream.of(sorts).map(Sort::toQuerySort).toList();
132+
this.sorts = sorts;
134133
}
135134

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

159163
hasMore = true;
160164
}
@@ -168,53 +172,47 @@ protected Iterator<T> doPageRead() {
168172
return Collections.emptyIterator();
169173
}
170174

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

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

180-
hasMore = queryResults.getHasMore();
181-
nextCursor = queryResults.getNextCursor();
180+
hasMore = result.hasMore();
181+
nextCursor = result.nextCursor();
182182

183-
return queryResults.getResults()
183+
return result.results()
184184
.stream()
185185
.map(NotionDatabaseItemReader::getProperties)
186186
.map(propertyMapper::map)
187187
.iterator();
188188
}
189189

190-
private static Map<String, String> getProperties(Page element) {
191-
return element.getProperties()
190+
private static Map<String, String> getProperties(Page page) {
191+
return page.properties()
192192
.entrySet()
193193
.stream()
194194
.collect(Collectors.toUnmodifiableMap(Entry::getKey, entry -> getPropertyValue(entry.getValue())));
195195
}
196196

197197
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-
};
198+
if (property instanceof RichTextProperty p) {
199+
return getPlainText(p.richText());
200+
}
201+
if (property instanceof TitleProperty p) {
202+
return getPlainText(p.title());
203+
}
204+
throw new IllegalArgumentException("Unsupported type: " + property.getClass());
203205
}
204206

205207
private static String getPlainText(List<RichText> texts) {
206-
return texts.isEmpty() ? "" : texts.get(0).getPlainText();
208+
return texts.isEmpty() ? "" : texts.get(0).plainText();
207209
}
208210

209211
/**
210212
* {@inheritDoc}
211213
*/
212-
@SuppressWarnings("DataFlowIssue")
213214
@Override
214215
protected void doClose() {
215-
client.close();
216-
client = null;
217-
218216
hasMore = false;
219217
}
220218

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: 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.springframework.batch.extensions.notion;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
6+
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
7+
import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
8+
import tools.jackson.databind.annotation.JsonNaming;
9+
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
@JsonNaming(SnakeCaseStrategy.class)
14+
record QueryResult(List<Page> results, String nextCursor, boolean hasMore) {
15+
16+
record Page(Map<String, PageProperty> properties) {
17+
18+
@JsonTypeInfo(use = Id.NAME, include = As.EXTERNAL_PROPERTY, property = "type")
19+
@JsonSubTypes({ //
20+
@JsonSubTypes.Type(name = "rich_text", value = PageProperty.RichTextProperty.class), //
21+
@JsonSubTypes.Type(name = "title", value = PageProperty.TitleProperty.class), //
22+
})
23+
interface PageProperty {
24+
25+
@JsonNaming(SnakeCaseStrategy.class)
26+
record RichTextProperty(List<PageProperty.RichText> richText) implements PageProperty {
27+
}
28+
29+
record TitleProperty(List<PageProperty.RichText> title) implements PageProperty {
30+
}
31+
32+
@JsonNaming(SnakeCaseStrategy.class)
33+
record RichText(String plainText) {
34+
}
35+
36+
}
37+
38+
}
39+
40+
}

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)