Skip to content

Commit 986ad26

Browse files
committed
pagination for mirror node introduced
1 parent 1f4eda8 commit 986ad26

File tree

7 files changed

+294
-20
lines changed

7 files changed

+294
-20
lines changed

hedera-base/src/main/java/com/openelements/hedera/base/NftRepository.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
import com.hedera.hashgraph.sdk.AccountId;
44
import com.hedera.hashgraph.sdk.TokenId;
5+
import com.openelements.hedera.base.mirrornode.Page;
56
import java.util.List;
67
import java.util.Optional;
78
import org.jspecify.annotations.NonNull;
89

910
/**
10-
* Interface for interacting with a Hedera network.
11-
* This interface provides methods for searching for NFTs.
11+
* Interface for interacting with a Hedera network. This interface provides methods for searching for NFTs.
1212
*/
1313
public interface NftRepository {
1414

@@ -30,12 +30,12 @@ public interface NftRepository {
3030
* @throws HederaException if the search fails
3131
*/
3232
@NonNull
33-
List<Nft> findByType(@NonNull TokenId tokenId) throws HederaException;
33+
Page<Nft> findByType(@NonNull TokenId tokenId) throws HederaException;
3434

3535
/**
3636
* Return the NFTs of a given type with the given serial.
3737
*
38-
* @param tokenId id of the token type
38+
* @param tokenId id of the token type
3939
* @param serialNumber serial of the nft instance
4040
* @return {@link Optional} containing the found NFT or null
4141
* @throws HederaException if the search fails
@@ -57,12 +57,13 @@ public interface NftRepository {
5757
/**
5858
* Return the NFT of a given type and serial owned by a specific account.
5959
*
60-
* @param owner id of the owner
61-
* @param tokenId id of the token type
60+
* @param owner id of the owner
61+
* @param tokenId id of the token type
6262
* @param serialNumber serial of the nft instance
6363
* @return {@link Optional} containing the found NFT or null
6464
* @throws HederaException if the search fails
6565
*/
6666
@NonNull
67-
Optional<Nft> findByOwnerAndTypeAndSerial(@NonNull AccountId owner, @NonNull TokenId tokenId, long serialNumber) throws HederaException;
67+
Optional<Nft> findByOwnerAndTypeAndSerial(@NonNull AccountId owner, @NonNull TokenId tokenId, long serialNumber)
68+
throws HederaException;
6869
}

hedera-base/src/main/java/com/openelements/hedera/base/implementation/NftRepositoryImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.openelements.hedera.base.Nft;
77
import com.openelements.hedera.base.NftRepository;
88
import com.openelements.hedera.base.mirrornode.MirrorNodeClient;
9+
import com.openelements.hedera.base.mirrornode.Page;
910
import java.util.List;
1011
import java.util.Objects;
1112
import java.util.Optional;
@@ -27,19 +28,21 @@ public List<Nft> findByOwner(@NonNull final AccountId owner) throws HederaExcept
2728

2829
@NonNull
2930
@Override
30-
public List<Nft> findByType(@NonNull final TokenId tokenId) throws HederaException {
31+
public Page<Nft> findByType(@NonNull final TokenId tokenId) throws HederaException {
3132
return mirrorNodeClient.queryNftsByTokenId(tokenId);
3233
}
3334

3435
@NonNull
3536
@Override
36-
public Optional<Nft> findByTypeAndSerial(@NonNull final TokenId tokenId, final long serialNumber) throws HederaException {
37+
public Optional<Nft> findByTypeAndSerial(@NonNull final TokenId tokenId, final long serialNumber)
38+
throws HederaException {
3739
return mirrorNodeClient.queryNftsByTokenIdAndSerial(tokenId, serialNumber);
3840
}
3941

4042
@NonNull
4143
@Override
42-
public List<Nft> findByOwnerAndType(@NonNull final AccountId owner, @NonNull final TokenId tokenId) throws HederaException {
44+
public List<Nft> findByOwnerAndType(@NonNull final AccountId owner, @NonNull final TokenId tokenId)
45+
throws HederaException {
4346
return mirrorNodeClient.queryNftsByAccountAndTokenId(owner, tokenId);
4447
}
4548

hedera-base/src/main/java/com/openelements/hedera/base/mirrornode/MirrorNodeClient.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@ public interface MirrorNodeClient {
1717
List<Nft> queryNftsByAccount(@NonNull AccountId accountId) throws HederaException;
1818

1919
@NonNull
20-
List<Nft> queryNftsByAccountAndTokenId(@NonNull AccountId accountId, @NonNull TokenId tokenId) throws HederaException;
20+
List<Nft> queryNftsByAccountAndTokenId(@NonNull AccountId accountId, @NonNull TokenId tokenId)
21+
throws HederaException;
2122

2223
@NonNull
23-
List<Nft> queryNftsByTokenId(@NonNull TokenId tokenId) throws HederaException;
24+
Page<Nft> queryNftsByTokenId(@NonNull TokenId tokenId) throws HederaException;
2425

2526
@NonNull
2627
Optional<Nft> queryNftsByTokenIdAndSerial(@NonNull TokenId tokenId, long serialNumber) throws HederaException;
2728

2829
@NonNull
29-
Optional<Nft> queryNftsByAccountAndTokenIdAndSerial(@NonNull AccountId accountId, @NonNull TokenId tokenId, long serialNumber) throws HederaException;
30+
Optional<Nft> queryNftsByAccountAndTokenIdAndSerial(@NonNull AccountId accountId, @NonNull TokenId tokenId,
31+
long serialNumber) throws HederaException;
3032

3133
@NonNull
3234
Optional<TransactionInfo> queryTransaction(@NonNull String transactionId) throws HederaException;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.openelements.hedera.base.mirrornode;
2+
3+
import java.util.List;
4+
5+
public interface Page<T> {
6+
7+
8+
int getNumber();
9+
10+
int getSize();
11+
12+
List<T> getData();
13+
14+
boolean hasNext();
15+
16+
Page<T> next();
17+
18+
Page<T> first();
19+
20+
boolean isFirst();
21+
}

hedera-spring/src/main/java/com/openelements/hedera/spring/implementation/MirrorNodeClientImpl.java

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.openelements.hedera.base.HederaException;
99
import com.openelements.hedera.base.Nft;
1010
import com.openelements.hedera.base.mirrornode.MirrorNodeClient;
11+
import com.openelements.hedera.base.mirrornode.Page;
1112
import com.openelements.hedera.base.mirrornode.TransactionInfo;
1213
import java.io.IOException;
1314
import java.net.URI;
@@ -47,7 +48,15 @@ public MirrorNodeClientImpl(@NonNull final String mirrorNodeEndpoint) {
4748
URL url = new URI(mirrorNodeEndpoint).toURL();
4849
mirrorNodeEndpointProtocol = url.getProtocol();
4950
mirrorNodeEndpointHost = url.getHost();
50-
mirrorNodeEndpointPort = url.getPort();
51+
if (mirrorNodeEndpointProtocol == "https" && url.getPort() == -1) {
52+
mirrorNodeEndpointPort = 443;
53+
} else if (mirrorNodeEndpointProtocol == "http" && url.getPort() == -1) {
54+
mirrorNodeEndpointPort = 80;
55+
} else if (url.getPort() == -1) {
56+
mirrorNodeEndpointPort = 443;
57+
} else {
58+
mirrorNodeEndpointPort = url.getPort();
59+
}
5160
} catch (Exception e) {
5261
throw new IllegalArgumentException("Error parsing mirrorNodeEndpoint '" + mirrorNodeEndpoint + "'", e);
5362
}
@@ -72,10 +81,16 @@ public List<Nft> queryNftsByAccountAndTokenId(@NonNull final AccountId accountId
7281
}
7382

7483
@Override
75-
public List<Nft> queryNftsByTokenId(@NonNull TokenId tokenId) throws HederaException {
76-
Objects.requireNonNull(tokenId, "tokenId must not be null");
77-
final JsonNode jsonNode = doGetCall("/api/v1/tokens/" + tokenId + "/nfts");
78-
return jsonNodeToNftList(jsonNode);
84+
public Page<Nft> queryNftsByTokenId(@NonNull TokenId tokenId) throws HederaException {
85+
final URI uri = URI.create(
86+
getUriPrefix()
87+
+ "/api/v1/tokens/" + tokenId + "/nfts");
88+
final Function<JsonNode, List<Nft>> dataExtractionFunction = node -> getNfts(node);
89+
final Function<JsonNode, URI> nextUriExtractionFunction = node -> getNextUri(node);
90+
91+
return new RestBasedPage<>(objectMapper, restClient,
92+
uri,
93+
dataExtractionFunction, nextUriExtractionFunction);
7994
}
8095

8196
@Override
@@ -186,4 +201,52 @@ private Nft jsonNodeToNft(final JsonNode jsonNode) throws IOException {
186201
}
187202
}
188203

204+
private List<Nft> getNfts(final JsonNode jsonNode) {
205+
if (!jsonNode.has("nfts")) {
206+
return List.of();
207+
}
208+
final JsonNode nftsNode = jsonNode.get("nfts");
209+
if (!nftsNode.isArray()) {
210+
throw new IllegalArgumentException("NFTs node is not an array: " + nftsNode);
211+
}
212+
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(nftsNode.iterator(), Spliterator.ORDERED),
213+
false)
214+
.map(nftNode -> {
215+
try {
216+
return jsonNodeToNft(nftNode);
217+
} catch (final Exception e) {
218+
throw new RuntimeException("Error parsing NFT from JSON '" + nftNode + "'", e);
219+
}
220+
}).toList();
221+
}
222+
223+
private String getUriPrefix() {
224+
return mirrorNodeEndpointProtocol + "://" + mirrorNodeEndpointHost + ":" + mirrorNodeEndpointPort;
225+
}
226+
227+
private URI getNextUri(final JsonNode jsonNode) {
228+
if (!jsonNode.has("links")) {
229+
return null;
230+
}
231+
final JsonNode linksNode = jsonNode.get("links");
232+
if (linksNode.isNull()) {
233+
return null;
234+
}
235+
if (!linksNode.has("next")) {
236+
return null;
237+
}
238+
final JsonNode nextNode = linksNode.get("next");
239+
if (nextNode.isNull()) {
240+
return null;
241+
}
242+
if (!nextNode.isTextual()) {
243+
throw new IllegalArgumentException("Next link is not a string: " + nextNode);
244+
}
245+
try {
246+
return new URI(getUriPrefix() + nextNode.asText());
247+
} catch (Exception e) {
248+
throw new IllegalArgumentException("Error parsing next link '" + nextNode.asText() + "'", e);
249+
}
250+
}
251+
189252
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.openelements.hedera.spring.implementation;
2+
3+
import static org.springframework.http.MediaType.APPLICATION_JSON;
4+
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.fasterxml.jackson.databind.JsonNode;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.openelements.hedera.base.mirrornode.Page;
9+
import java.net.URI;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import java.util.Objects;
13+
import java.util.function.Function;
14+
import org.jspecify.annotations.NonNull;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
import org.springframework.http.HttpStatusCode;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.web.client.RestClient;
20+
21+
public class RestBasedPage<T> implements Page<T> {
22+
23+
private final static Logger log = LoggerFactory.getLogger(RestBasedPage.class);
24+
25+
26+
private final ObjectMapper objectMapper;
27+
28+
private final RestClient restClient;
29+
30+
private final Function<JsonNode, List<T>> dataExtractionFunction;
31+
32+
private final Function<JsonNode, URI> nextUriExtractionFunction;
33+
34+
private final int number;
35+
36+
private final List<T> data;
37+
38+
private final URI nextUri;
39+
40+
private final URI firstUri;
41+
42+
private final URI currentUri;
43+
44+
public RestBasedPage(final @NonNull ObjectMapper objectMapper, final @NonNull RestClient restClient,
45+
final @NonNull URI uri,
46+
final @NonNull Function<JsonNode, List<T>> dataExtractionFunction,
47+
final @NonNull Function<JsonNode, URI> nextUriExtractionFunction) {
48+
this(objectMapper, restClient, uri, 0, dataExtractionFunction, nextUriExtractionFunction, uri);
49+
}
50+
51+
public RestBasedPage(final @NonNull ObjectMapper objectMapper, final @NonNull RestClient restClient,
52+
final @NonNull URI uri, int number,
53+
final @NonNull Function<JsonNode, List<T>> dataExtractionFunction,
54+
final @NonNull Function<JsonNode, URI> nextUriExtractionFunction,
55+
final @NonNull URI firstUri) {
56+
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
57+
this.restClient = Objects.requireNonNull(restClient, "restClient must not be null");
58+
this.dataExtractionFunction = Objects.requireNonNull(dataExtractionFunction,
59+
"dataExtractionFunction must not be null");
60+
this.nextUriExtractionFunction = Objects.requireNonNull(nextUriExtractionFunction,
61+
"nextUriExtractionFunction must not be null");
62+
this.firstUri = Objects.requireNonNull(firstUri, "firstUri must not be null");
63+
this.currentUri = Objects.requireNonNull(uri, "uri must not be null");
64+
this.number = number;
65+
if (number < 0) {
66+
throw new IllegalArgumentException("number must be non-negative");
67+
}
68+
log.debug("Fetching data from URI: {}", uri);
69+
final ResponseEntity<String> response = restClient.get()
70+
.uri(uri).accept(APPLICATION_JSON)
71+
.retrieve()
72+
.toEntity(String.class);
73+
final HttpStatusCode statusCode = response.getStatusCode();
74+
if (!statusCode.is2xxSuccessful()) {
75+
throw new IllegalStateException("HTTP status code: " + statusCode);
76+
}
77+
final String body = response.getBody();
78+
if (body == null) {
79+
throw new IllegalStateException("Response body is null");
80+
}
81+
try {
82+
final JsonNode jsonNode = objectMapper.readTree(body);
83+
data = Collections.unmodifiableList(dataExtractionFunction.apply(jsonNode));
84+
nextUri = nextUriExtractionFunction.apply(jsonNode);
85+
} catch (JsonProcessingException e) {
86+
throw new RuntimeException("JSON parsing error", e);
87+
}
88+
}
89+
90+
@Override
91+
public int getNumber() {
92+
return number;
93+
}
94+
95+
@Override
96+
public int getSize() {
97+
return data.size();
98+
}
99+
100+
@Override
101+
public List<T> getData() {
102+
return data;
103+
}
104+
105+
@Override
106+
public boolean hasNext() {
107+
return nextUri != null;
108+
}
109+
110+
@Override
111+
public Page<T> next() {
112+
if (nextUri == null) {
113+
throw new IllegalStateException("No next URI");
114+
}
115+
return new RestBasedPage<>(objectMapper, restClient, nextUri, number + 1, dataExtractionFunction,
116+
nextUriExtractionFunction, firstUri);
117+
}
118+
119+
@Override
120+
public Page<T> first() {
121+
return new RestBasedPage<>(objectMapper, restClient, firstUri, dataExtractionFunction,
122+
nextUriExtractionFunction);
123+
}
124+
125+
@Override
126+
public boolean isFirst() {
127+
return Objects.equals(firstUri, currentUri);
128+
}
129+
}

0 commit comments

Comments
 (0)