Skip to content

Commit ce5a25e

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 ce5a25e

File tree

12 files changed

+437
-172
lines changed

12 files changed

+437
-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: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,21 @@
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.PageProperty.RichText;
21+
import org.springframework.batch.extensions.notion.PageProperty.RichTextProperty;
22+
import org.springframework.batch.extensions.notion.PageProperty.TitleProperty;
2923
import org.springframework.batch.extensions.notion.mapping.PropertyMapper;
3024
import org.springframework.batch.infrastructure.item.ExecutionContext;
3125
import org.springframework.batch.infrastructure.item.ItemReader;
3226
import org.springframework.batch.infrastructure.item.data.AbstractPaginatedDataItemReader;
27+
import org.springframework.http.HttpHeaders;
3328
import org.springframework.util.Assert;
29+
import org.springframework.web.client.ApiVersionInserter;
30+
import org.springframework.web.client.RestClient;
31+
import org.springframework.web.client.support.RestClientAdapter;
32+
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
3433

3534
import java.util.Collections;
3635
import java.util.Iterator;
@@ -39,7 +38,6 @@
3938
import java.util.Map.Entry;
4039
import java.util.Objects;
4140
import java.util.stream.Collectors;
42-
import java.util.stream.Stream;
4341

4442
/**
4543
* Restartable {@link ItemReader} that reads entries from a Notion database via a paging
@@ -71,11 +69,11 @@ public class NotionDatabaseItemReader<T> extends AbstractPaginatedDataItemReader
7169

7270
private String baseUrl = DEFAULT_BASE_URL;
7371

74-
private @Nullable QueryTopLevelFilter filter;
72+
private @Nullable Filter filter;
7573

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

78-
private @Nullable NotionClient client;
76+
private @Nullable NotionDatabaseService service;
7977

8078
private boolean hasMore;
8179

@@ -117,7 +115,7 @@ public void setBaseUrl(String baseUrl) {
117115
* @see Filter#where(Filter)
118116
*/
119117
public void setFilter(Filter filter) {
120-
this.filter = filter.toQueryTopLevelFilter();
118+
this.filter = filter;
121119
}
122120

123121
/**
@@ -130,7 +128,7 @@ public void setFilter(Filter filter) {
130128
* @see Sort#by(Sort.Timestamp)
131129
*/
132130
public void setSorts(Sort... sorts) {
133-
this.sorts = Stream.of(sorts).map(Sort::toQuerySort).toList();
131+
this.sorts = sorts;
134132
}
135133

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

159162
hasMore = true;
160163
}
@@ -168,53 +171,47 @@ protected Iterator<T> doPageRead() {
168171
return Collections.emptyIterator();
169172
}
170173

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

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

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

183-
return queryResults.getResults()
182+
return result.results()
184183
.stream()
185184
.map(NotionDatabaseItemReader::getProperties)
186185
.map(propertyMapper::map)
187186
.iterator();
188187
}
189188

190-
private static Map<String, String> getProperties(Page element) {
191-
return element.getProperties()
189+
private static Map<String, String> getProperties(Page page) {
190+
return page.properties()
192191
.entrySet()
193192
.stream()
194193
.collect(Collectors.toUnmodifiableMap(Entry::getKey, entry -> getPropertyValue(entry.getValue())));
195194
}
196195

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

205206
private static String getPlainText(List<RichText> texts) {
206-
return texts.isEmpty() ? "" : texts.get(0).getPlainText();
207+
return texts.isEmpty() ? "" : texts.get(0).plainText();
207208
}
208209

209210
/**
210211
* {@inheritDoc}
211212
*/
212-
@SuppressWarnings("DataFlowIssue")
213213
@Override
214214
protected void doClose() {
215-
client.close();
216-
client = null;
217-
218215
hasMore = false;
219216
}
220217

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

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)