• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codenotary / immudb4j / #149

26 Jul 2026 09:37AM UTC coverage: 82.814% (+0.5%) from 82.32%
#149

push

github

web-flow
update ImmuClientverifiedGet  to return  `VerifiableEntry` instead of`Entry` (#79)

* Update ImmuClient.verifiedGet to return instance of VerifiableEntry as per schema.proto rpc VerifiableGet

(cherry picked from commit be6c674c5)

* Add override methods documentation, add new constructor for Entry which supports all current properties and make Entry immutable

65 of 68 new or added lines in 5 files covered. (95.59%)

104 existing lines in 1 file now uncovered.

1595 of 1926 relevant lines covered (82.81%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.72
/src/main/java/io/codenotary/immudb4j/ImmuClient.java
1
/*
2
Copyright 2022 CodeNotary, Inc. All rights reserved.
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
        http://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 io.codenotary.immudb4j;
17

18
import com.google.protobuf.ByteString;
19
import com.google.protobuf.Empty;
20
import io.codenotary.immudb.ImmuServiceGrpc;
21
import io.codenotary.immudb.ImmudbProto;
22
import io.codenotary.immudb.ImmudbProto.Chunk;
23
import io.codenotary.immudb.ImmudbProto.NamedParam;
24
import io.codenotary.immudb.ImmudbProto.NewTxResponse;
25
import io.codenotary.immudb.ImmudbProto.ScanRequest;
26
import io.codenotary.immudb.ImmudbProto.Score;
27
import io.codenotary.immudb.ImmudbProto.TxMode;
28
import io.codenotary.immudb4j.basics.LatchHolder;
29
import io.codenotary.immudb4j.crypto.CryptoUtils;
30
import io.codenotary.immudb4j.crypto.DualProof;
31
import io.codenotary.immudb4j.crypto.InclusionProof;
32
import io.codenotary.immudb4j.exceptions.*;
33
import io.codenotary.immudb4j.sql.SQLException;
34
import io.codenotary.immudb4j.sql.SQLQueryResult;
35
import io.codenotary.immudb4j.sql.SQLValue;
36
import io.codenotary.immudb4j.user.Permission;
37
import io.codenotary.immudb4j.user.User;
38

39
import io.grpc.ConnectivityState;
40
import io.grpc.ManagedChannel;
41
import io.grpc.ManagedChannelBuilder;
42
import io.grpc.Status;
43
import io.grpc.StatusRuntimeException;
44
import io.grpc.stub.StreamObserver;
45

46
import java.nio.ByteBuffer;
47
import java.nio.ByteOrder;
48
import java.nio.charset.StandardCharsets;
49
import java.security.NoSuchAlgorithmException;
50
import java.security.PublicKey;
51
import java.util.ArrayList;
52
import java.util.Arrays;
53
import java.util.HashMap;
54
import java.util.Iterator;
55
import java.util.List;
56
import java.util.Map;
57
import java.util.Timer;
58
import java.util.TimerTask;
59
import java.util.concurrent.TimeUnit;
60
import java.util.function.Supplier;
61
import java.util.stream.Collectors;
62

63
/**
64
 * The official immudb Java Client.
65
 *
66
 * @author Jeronimo Irazabal
67
 * @author Marius Ileana
68
 */
69
public class ImmuClient {
70

71
    private final PublicKey serverSigningKey;
72
    private final ImmuStateHolder stateHolder;
73
    private long keepAlivePeriod;
74
    private int chunkSize;
75

76
    private ManagedChannel channel;
77

78
    private final ImmuServiceGrpc.ImmuServiceBlockingStub blockingStub;
79
    private final ImmuServiceGrpc.ImmuServiceStub nonBlockingStub;
80

81
    private Session session;
82
    private Timer sessionHeartBeat;
83

84
    private ImmuClient(Builder builder) {
1✔
85
        stateHolder = builder.getStateHolder();
1✔
86
        serverSigningKey = builder.getServerSigningKey();
1✔
87
        keepAlivePeriod = builder.getKeepAlivePeriod();
1✔
88
        chunkSize = builder.getChunkSize();
1✔
89

90
        channel = ManagedChannelBuilder
1✔
91
                .forAddress(builder.getServerUrl(), builder.getServerPort())
1✔
92
                .usePlaintext()
1✔
93
                .intercept(new ImmudbAuthRequestInterceptor(this))
1✔
94
                .build();
1✔
95

96
        blockingStub = ImmuServiceGrpc.newBlockingStub(channel);
1✔
97
        nonBlockingStub = ImmuServiceGrpc.newStub(channel);
1✔
98
    }
1✔
99

100
    /**
101
     * @return ImmuClient builder for chaining client settings
102
     */
103
    public static Builder newBuilder() {
104
        return new Builder();
1✔
105
    }
106

107
    /**
108
     * Releases the resources used by the SDK objects. (e.g. connection resources).
109
     * This method should be called just before the existing process ends.
110
     */
111
    public synchronized void shutdown() throws InterruptedException {
112
        if (channel == null) {
1✔
113
            return;
1✔
114
        }
115

116
        if (session != null) {
1✔
117
            closeSession();
1✔
118
        }
119

120
        try {
121
            channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
1✔
122
        } finally {
123
            channel = null;
1✔
124
        }
125
    }
1✔
126

127
    protected synchronized Session getSession() {
128
        return session;
1✔
129
    }
130

131
    /**
132
     * Establishes a new database session using provided credentials
133
     * 
134
     * @param database The name of the database to which the session is established
135
     * @param username The username is required to get authorization
136
     * @param password The password is required to get authorization
137
     */
138
    public synchronized void openSession(String database, String username, String password) {
139
        if (session != null) {
1✔
140
            throw new IllegalStateException("session already opened");
1✔
141
        }
142

143
        final ImmudbProto.OpenSessionRequest req = ImmudbProto.OpenSessionRequest
144
                .newBuilder()
1✔
145
                .setDatabaseName(database)
1✔
146
                .setUsername(Utils.toByteString(username))
1✔
147
                .setPassword(Utils.toByteString(password))
1✔
148
                .build();
1✔
149

150
        final ImmudbProto.OpenSessionResponse resp = this.blockingStub.openSession(req);
1✔
151

152
        session = new Session(resp.getSessionID(), database);
1✔
153

154
        sessionHeartBeat = new Timer(true);
1✔
155

156
        sessionHeartBeat.schedule(new TimerTask() {
1✔
157
            @Override
158
            public void run() {
159
                try {
160
                    synchronized (ImmuClient.this) {
1✔
161
                        if (session != null) {
1✔
162
                            blockingStub.keepAlive(Empty.getDefaultInstance());
1✔
163
                        }
164
                    }
1✔
UNCOV
165
                } catch (Exception e) {
×
UNCOV
166
                    e.printStackTrace();
×
167
                }
1✔
168
            }
1✔
169
        }, 0, keepAlivePeriod);
170
    }
1✔
171

172
    /**
173
     * Closes the open database session
174
     */
175
    public synchronized void closeSession() {
176
        if (session == null) {
1✔
177
            throw new IllegalStateException("no open session");
1✔
178
        }
179

180
        sessionHeartBeat.cancel();
1✔
181

182
        try {
183
            blockingStub.closeSession(Empty.getDefaultInstance());
1✔
184
        } finally {
185
            session = null;
1✔
186
        }
187
    }
1✔
188

189
    /**
190
     * Get the locally saved state of the current database.
191
     * If nothing exists already, it is fetched from the server and save it locally.
192
     */
193
    private ImmuState state() throws VerificationException {
194
        if (session == null) {
1✔
UNCOV
195
            throw new IllegalStateException("no open session");
×
196
        }
197

198
        ImmuState state = stateHolder.getState(session.getDatabase());
1✔
199

200
        if (state == null) {
1✔
201
            state = currentState();
1✔
202
            stateHolder.setState(state);
1✔
203
        }
204

205
        return state;
1✔
206
    }
207

208
    /**
209
     * Get the current database state that exists on the server.
210
     * It may throw VerificationException if server's state signature verification
211
     * fails
212
     * (if this feature is enabled on the client side, at least).
213
     * Note: local state is not updated because this is not a verified operation
214
     */
215
    public synchronized ImmuState currentState() throws VerificationException {
216
        if (session == null) {
1✔
217
            throw new IllegalStateException("no open session");
1✔
218
        }
219

220
        final ImmudbProto.ImmutableState state = blockingStub.currentState(Empty.getDefaultInstance());
1✔
221

222
        final ImmuState immuState = ImmuState.valueOf(state);
1✔
223

224
        if (!session.getDatabase().equals(immuState.getDatabase())) {
1✔
UNCOV
225
            throw new VerificationException("database mismatch");
×
226
        }
227

228
        if (!immuState.checkSignature(serverSigningKey)) {
1✔
229
            throw new VerificationException("State signature verification failed");
1✔
230
        }
231

232
        return immuState;
1✔
233
    }
234

235
    //
236
    // ========== SQL ==========
237
    //
238

239
    /**
240
     * Creates a new SQL transaction that can be terminated with the
241
     * {@link #commitTransaction() commit} or {@link #rollbackTransaction()
242
     * rollback} methods
243
     * 
244
     * @throws SQLException if the cause of the error is sql-related
245
     */
246
    public synchronized void beginTransaction() throws SQLException {
247
        if (session == null) {
1✔
UNCOV
248
            throw new IllegalStateException("no open session");
×
249
        }
250

251
        if (session.getTransactionID() != null) {
1✔
UNCOV
252
            throw new IllegalStateException("transaction already initiated");
×
253
        }
254

255
        final ImmudbProto.NewTxRequest req = ImmudbProto.NewTxRequest.newBuilder()
1✔
256
                .setMode(TxMode.ReadWrite)
1✔
257
                .build();
1✔
258

259
        final NewTxResponse res = blockingStub.newTx(req);
1✔
260

261
        session.setTransactionID(res.getTransactionID());
1✔
262
    }
1✔
263

264
    /**
265
     * Commits the ongoing SQL transaction
266
     * 
267
     * @throws SQLException if the cause of the error is sql-related
268
     */
269
    public synchronized void commitTransaction() throws SQLException {
270
        if (session == null) {
1✔
UNCOV
271
            throw new IllegalStateException("no open session");
×
272
        }
273

274
        if (session.getTransactionID() == null) {
1✔
UNCOV
275
            throw new IllegalStateException("no transaction initiated");
×
276
        }
277

278
        blockingStub.commit(Empty.getDefaultInstance());
1✔
279

280
        session.setTransactionID(null);
1✔
281
    }
1✔
282

283
    /**
284
     * Rollback the ongoing SQL transaction
285
     * 
286
     * @throws SQLException if the cause of the error is sql-related
287
     */
288
    public synchronized void rollbackTransaction() throws SQLException {
UNCOV
289
        if (session == null) {
×
290
            throw new IllegalStateException("no open session");
×
291
        }
292

UNCOV
293
        if (session.getTransactionID() == null) {
×
294
            throw new IllegalStateException("no transaction initiated");
×
295
        }
296

297
        blockingStub.rollback(Empty.getDefaultInstance());
×
298

UNCOV
299
        session.setTransactionID(null);
×
UNCOV
300
    }
×
301

302
    /**
303
     * Executes a SQL statement in the ongoing SQL transaction
304
     * 
305
     * @param stmt   the SQL statement to be executed
306
     * @param params the positional parameters for SQL statement evaluation
307
     * 
308
     * @throws SQLException if the cause of the error is sql-related
309
     */
310
    public void sqlExec(String stmt, SQLValue... params) throws SQLException {
311
        sqlExec(stmt, sqlNameParams(params));
1✔
312
    }
1✔
313

314
    /**
315
     * Executes a SQL statement in the ongoing SQL transaction
316
     * 
317
     * @param stmt   the SQL statement to be executed
318
     * @param params the named parameters for SQL statement evaluation
319
     * 
320
     * @throws SQLException if the cause of the error is sql-related
321
     */
322
    public synchronized void sqlExec(String stmt, Map<String, SQLValue> params) throws SQLException {
323
        if (session == null) {
1✔
UNCOV
324
            throw new IllegalStateException("no open session");
×
325
        }
326

327
        if (session.getTransactionID() == null) {
1✔
UNCOV
328
            throw new IllegalStateException("no transaction initiated");
×
329
        }
330

331
        final ImmudbProto.SQLExecRequest req = ImmudbProto.SQLExecRequest.newBuilder()
1✔
332
                .setSql(stmt)
1✔
333
                .addAllParams(sqlEncodeParams(params))
1✔
334
                .build();
1✔
335

336
        blockingStub.txSQLExec(req);
1✔
337
    }
1✔
338

339
    /**
340
     * Performs a SQL query in the ongoing SQL transaction
341
     * 
342
     * @param stmt   the SQL query to be evaluated
343
     * @param params the positional parameters for SQL statement evaluation
344
     * 
345
     * @return the query resultset
346
     * 
347
     * @throws SQLException if the cause of the error is sql-related
348
     */
349
    public SQLQueryResult sqlQuery(String stmt, SQLValue... params) throws SQLException {
350
        return sqlQuery(stmt, sqlNameParams(params));
1✔
351
    }
352

353
    /**
354
     * Performs a SQL query in the ongoing SQL transaction
355
     * 
356
     * @param stmt   the SQL query to be evaluated
357
     * @param params the named parameters for SQL statement evaluation
358
     * 
359
     * @return the query resultset
360
     * 
361
     * @throws SQLException if the cause of the error is sql-related
362
     */
363
    public synchronized SQLQueryResult sqlQuery(String stmt, Map<String, SQLValue> params) throws SQLException {
364
        if (session == null) {
1✔
UNCOV
365
            throw new IllegalStateException("no open session");
×
366
        }
367

368
        if (session.getTransactionID() == null) {
1✔
369
            throw new IllegalStateException("no transaction initiated");
1✔
370
        }
371

372
        final ImmudbProto.SQLQueryRequest req = ImmudbProto.SQLQueryRequest.newBuilder()
1✔
373
                .setSql(stmt)
1✔
374
                .addAllParams(sqlEncodeParams(params))
1✔
375
                .setAcceptStream(true)
1✔
376
                .build();
1✔
377

378
        Iterator<io.codenotary.immudb.ImmudbProto.SQLQueryResult> it = blockingStub.txSQLQuery(req);
1✔
379
        return new SQLQueryResult(it);
1✔
380
    }
381

382
    private Map<String, SQLValue> sqlNameParams(SQLValue... params) {
383
        final Map<String, SQLValue> nparams = new HashMap<>(params.length);
1✔
384

385
        for (int i = 1; i <= params.length; i++) {
1✔
386
            nparams.put("param" + i, params[i - 1]);
1✔
387
        }
388

389
        return nparams;
1✔
390
    }
391

392
    /**
393
     * Performs a SQL query WITHOUT requiring an active transaction.
394
     * This method uses the streaming SQLQuery gRPC call instead of TxSQLQuery,
395
     * allowing access to full database history including HISTORY OF queries.
396
     * 
397
     * Use this method when you need to query historical data with HISTORY OF
398
     * that spans beyond the current transaction snapshot.
399
     * 
400
     * @param stmt   the SQL query to be evaluated
401
     * @param params the positional parameters for SQL statement evaluation
402
     * 
403
     * @return the query resultset
404
     * 
405
     * @throws SQLException if the cause of the error is sql-related
406
     */
407
    public SQLQueryResult sqlQueryWithoutTx(String stmt, SQLValue... params) throws SQLException {
408
        return sqlQueryWithoutTx(stmt, sqlNameParams(params));
1✔
409
    }
410

411
    /**
412
     * Performs a SQL query WITHOUT requiring an active transaction.
413
     * This method uses the streaming SQLQuery gRPC call instead of TxSQLQuery,
414
     * allowing access to full database history including HISTORY OF queries.
415
     * 
416
     * Use this method when you need to query historical data with HISTORY OF
417
     * that spans beyond the current transaction snapshot.
418
     * 
419
     * @param stmt   the SQL query to be evaluated
420
     * @param params the named parameters for SQL statement evaluation
421
     * 
422
     * @return the query resultset
423
     * 
424
     * @throws SQLException if the cause of the error is sql-related
425
     */
426
    public synchronized SQLQueryResult sqlQueryWithoutTx(String stmt, Map<String, SQLValue> params) throws SQLException {
427
        if (session == null) {
1✔
428
            throw new IllegalStateException("no open session");
1✔
429
        }
430

431
        final ImmudbProto.SQLQueryRequest req = ImmudbProto.SQLQueryRequest.newBuilder()
1✔
432
                .setSql(stmt)
1✔
433
                .addAllParams(sqlEncodeParams(params))
1✔
434
                .setAcceptStream(true)
1✔
435
                .build();
1✔
436

437
        // Use SQLQuery (streaming) instead of TxSQLQuery (transaction-based)
438
        // This allows access to full history including HISTORY OF queries
439
        Iterator<io.codenotary.immudb.ImmudbProto.SQLQueryResult> it = blockingStub.sQLQuery(req);
1✔
440
        return new SQLQueryResult(it);
1✔
441
    }
442

443
    private Iterable<NamedParam> sqlEncodeParams(Map<String, SQLValue> params) {
444
        List<ImmudbProto.NamedParam> nparams = new ArrayList<>();
1✔
445

446
        for (Map.Entry<String, SQLValue> p : params.entrySet()) {
1✔
447
            nparams.add(NamedParam.newBuilder()
1✔
448
                    .setName(p.getKey())
1✔
449
                    .setValue(p.getValue().asProtoSQLValue())
1✔
450
                    .build());
1✔
451
        }
1✔
452

453
        return nparams;
1✔
454
    }
455

456
    //
457
    // ========== DATABASE ==========
458
    //
459

460
    /**
461
     * Creates a database using default settings
462
     * 
463
     * @param database the database name
464
     */
465
    public void createDatabase(String database) {
466
        createDatabase(database, false);
1✔
467
    }
1✔
468

469
    /**
470
     * Creates a database using default settings
471
     * 
472
     * @param database    the database name
473
     * @param ifNotExists allow the database to be already created
474
     */
475
    public synchronized void createDatabase(String database, boolean ifNotExists) {
476
        if (session == null) {
1✔
477
            throw new IllegalStateException("no open session");
1✔
478
        }
479

480
        final ImmudbProto.CreateDatabaseRequest req = ImmudbProto.CreateDatabaseRequest.newBuilder()
1✔
481
                .setName(database)
1✔
482
                .setIfNotExists(ifNotExists)
1✔
483
                .build();
1✔
484

485
        blockingStub.createDatabaseV2(req);
1✔
486
    }
1✔
487

488
    /**
489
     * Loads database on the server. A database is not loaded
490
     * if it has AutoLoad setting set to false or if it failed to load during immudb
491
     * startup.
492
     * 
493
     * This call requires SysAdmin permission level or admin permission to the
494
     * database.
495
     * 
496
     * @param database the database name
497
     */
498
    public synchronized void loadDatabase(String database) {
499
        if (session == null) {
1✔
500
            throw new IllegalStateException("no open session");
1✔
501
        }
502

503
        final ImmudbProto.LoadDatabaseRequest req = ImmudbProto.LoadDatabaseRequest.newBuilder()
1✔
504
                .setDatabase(database)
1✔
505
                .build();
1✔
506

UNCOV
507
        blockingStub.loadDatabase(req);
×
UNCOV
508
    }
×
509

510
    /**
511
     * Unloads database on the server. Such database becomes inaccessible by the
512
     * client and server frees internal resources allocated for that database.
513
     * 
514
     * This call requires SysAdmin permission level or admin permission to the
515
     * database.
516
     * 
517
     * @param database the database name
518
     */
519
    public synchronized void unloadDatabase(String database) {
520
        if (session == null) {
1✔
521
            throw new IllegalStateException("no open session");
1✔
522
        }
523

524
        final ImmudbProto.UnloadDatabaseRequest req = ImmudbProto.UnloadDatabaseRequest.newBuilder()
1✔
525
                .setDatabase(database)
1✔
526
                .build();
1✔
527

528
        blockingStub.unloadDatabase(req);
1✔
529
    }
1✔
530

531
    /**
532
     * Removes an unloaded database. This also removes locally stored files used by
533
     * the database.
534
     * 
535
     * This call requires SysAdmin permission level or admin permission to the
536
     * database.
537
     * 
538
     * @param database the database name
539
     */
540
    public synchronized void deleteDatabase(String database) {
541
        if (session == null) {
1✔
542
            throw new IllegalStateException("no open session");
1✔
543
        }
544

545
        final ImmudbProto.DeleteDatabaseRequest req = ImmudbProto.DeleteDatabaseRequest.newBuilder()
1✔
546
                .setDatabase(database)
1✔
547
                .build();
1✔
548

549
        blockingStub.deleteDatabase(req);
1✔
550
    }
1✔
551

552
    /**
553
     * @return the list of existing databases
554
     */
555
    public synchronized List<Database> databases() {
556
        if (session == null) {
1✔
557
            throw new IllegalStateException("no open session");
1✔
558
        }
559

560
        final ImmudbProto.DatabaseListRequestV2 req = ImmudbProto.DatabaseListRequestV2.newBuilder().build();
1✔
561
        final ImmudbProto.DatabaseListResponseV2 resp = blockingStub.databaseListV2(req);
1✔
562

563
        final List<Database> list = new ArrayList<>(resp.getDatabasesCount());
1✔
564

565
        for (ImmudbProto.DatabaseWithSettings db : resp.getDatabasesList()) {
1✔
566
            list.add(Database.valueOf(db));
1✔
567
        }
1✔
568

569
        return list;
1✔
570
    }
571

572
    //
573
    // ========== GET ==========
574
    //
575

576
    /**
577
     * @param key the key to look for
578
     * @return the latest entry associated to the provided key
579
     * @throws KeyNotFoundException if the key is not found
580
     */
581
    public Entry get(String key) throws KeyNotFoundException {
582
        return get(Utils.toByteArray(key));
1✔
583
    }
584

585
    /**
586
     * @param key the key to look for
587
     * @return the latest entry associated to the provided key
588
     * @throws KeyNotFoundException if the key is not found
589
     */
590
    public Entry get(byte[] key) throws KeyNotFoundException {
591
        return getAtTx(key, 0);
1✔
592
    }
593

594
    /**
595
     * @param key the key to look for
596
     * @param tx  the transaction at which the associated entry is expected to be
597
     * @return the entry associated to the provided key and transaction
598
     * @throws KeyNotFoundException if the key is not found
599
     */
600
    public Entry getAtTx(String key, long tx) throws KeyNotFoundException {
601
        return getAtTx(Utils.toByteArray(key), tx);
1✔
602
    }
603

604
    /**
605
     * @param key the key to look for
606
     * @param tx  the transaction at which the associated entry is expected to be
607
     * @return the entry associated to the provided key and transaction
608
     * @throws KeyNotFoundException if the key is not found
609
     */
610
    public synchronized Entry getAtTx(byte[] key, long tx) throws KeyNotFoundException {
611
        final ImmudbProto.KeyRequest req = ImmudbProto.KeyRequest.newBuilder()
1✔
612
                .setKey(Utils.toByteString(key))
1✔
613
                .setAtTx(tx)
1✔
614
                .build();
1✔
615

616
        try {
617
            return Entry.valueOf(blockingStub.get(req));
1✔
618
        } catch (StatusRuntimeException e) {
1✔
619
            if (e.getMessage().contains("key not found")) {
1✔
620
                throw new KeyNotFoundException();
1✔
621
            }
622

UNCOV
623
            throw e;
×
624
        }
625
    }
626

627
    /**
628
     * This method can be used to avoid additional latency while the server
629
     * waits for the indexer to finish indexing but still guarantees that
630
     * specific portion of the database history has already been indexed.
631
     * 
632
     * @param key the key to look for
633
     * @param tx  the lowest transaction from which the associated entry should be
634
     *            retrieved
635
     * @return the latest indexed entry associated the to provided key. Ensuring the
636
     *         indexing has already be completed up to the specified transaction.
637
     * @throws KeyNotFoundException if the key is not found
638
     */
639
    public Entry getSinceTx(String key, long tx) throws KeyNotFoundException {
640
        return getSinceTx(Utils.toByteArray(key), tx);
1✔
641
    }
642

643
    /**
644
     * This method can be used to avoid additional latency while the server
645
     * waits for the indexer to finish indexing but still guarantees that
646
     * specific portion of the database history has already been indexed.
647
     * 
648
     * @param key the key to look for
649
     * @param tx  the lowest transaction from which the associated entry should be
650
     *            retrieved
651
     * @return the latest indexed entry associated the to provided key. Ensuring the
652
     *         indexing has already be completed up to the specified transaction.
653
     * @throws KeyNotFoundException if the key is not found
654
     */
655
    public synchronized Entry getSinceTx(byte[] key, long tx) throws KeyNotFoundException {
656
        final ImmudbProto.KeyRequest req = ImmudbProto.KeyRequest.newBuilder()
1✔
657
                .setKey(Utils.toByteString(key))
1✔
658
                .setSinceTx(tx)
1✔
659
                .build();
1✔
660

661
        try {
662
            return Entry.valueOf(blockingStub.get(req));
1✔
663
        } catch (StatusRuntimeException e) {
1✔
664
            if (e.getMessage().contains("key not found")) {
1✔
665
                throw new KeyNotFoundException();
1✔
666
            }
667

UNCOV
668
            throw e;
×
669
        }
670
    }
671

672
    /**
673
     * @param key the key to look for
674
     * @param rev the specific revision
675
     * @return the specific revision for given key.
676
     * 
677
     *         Key revision is an integer value that starts at 1 when
678
     *         the key is created and then increased by 1 on every update made to
679
     *         that key.
680
     * 
681
     *         The way rev is interpreted depends on the value:
682
     *         - if rev == 0, returns current value
683
     *         - if rev &gt; 0, returns nth revision value, e.g. 1 is the first
684
     *         value,
685
     *         2 is the second and so on
686
     *         - if rev &lt; 0, returns nth revision value from the end, e.g. -1 is
687
     *         the
688
     *         previous value, -2 is the one before and so on
689
     * @throws KeyNotFoundException if the key is not found
690
     */
691
    public Entry getAtRevision(String key, long rev) throws KeyNotFoundException {
692
        return getAtRevision(Utils.toByteArray(key), rev);
1✔
693
    }
694

695
    /**
696
     * @param key the key to look for
697
     * @param rev the specific revision
698
     * @return the specific revision for given key.
699
     * 
700
     *         Key revision is an integer value that starts at 1 when
701
     *         the key is created and then increased by 1 on every update made to
702
     *         that key.
703
     * 
704
     *         The way rev is interpreted depends on the value:
705
     *         - if rev == 0, returns current value
706
     *         - if rev &gt; 0, returns nth revision value, e.g. 1 is the first
707
     *         value,
708
     *         2 is the second and so on
709
     *         - if rev &lt; 0, returns nth revision value from the end, e.g. -1 is
710
     *         the
711
     *         previous value, -2 is the one before and so on
712
     * @throws KeyNotFoundException if the key is not found
713
     */
714
    public synchronized Entry getAtRevision(byte[] key, long rev) throws KeyNotFoundException {
715
        final ImmudbProto.KeyRequest req = ImmudbProto.KeyRequest.newBuilder()
1✔
716
                .setKey(Utils.toByteString(key))
1✔
717
                .setAtRevision(rev)
1✔
718
                .build();
1✔
719

720
        try {
721
            return Entry.valueOf(blockingStub.get(req));
1✔
722
        } catch (StatusRuntimeException e) {
1✔
723
            if (e.getMessage().contains("key not found")) {
1✔
UNCOV
724
                throw new KeyNotFoundException();
×
725
            }
726

727
            throw e;
1✔
728
        }
729
    }
730

731
    /**
732
     * @param keys the lists of keys to look for
733
     * @return retrieves multiple entries in a single call
734
     */
735
    public synchronized List<Entry> getAll(List<String> keys) {
736
        final List<ByteString> keysBS = new ArrayList<>(keys.size());
1✔
737

738
        for (String key : keys) {
1✔
739
            keysBS.add(Utils.toByteString(key));
1✔
740
        }
1✔
741

742
        final ImmudbProto.KeyListRequest req = ImmudbProto.KeyListRequest.newBuilder().addAllKeys(keysBS).build();
1✔
743
        final ImmudbProto.Entries entries = blockingStub.getAll(req);
1✔
744

745
        final List<Entry> result = new ArrayList<>(entries.getEntriesCount());
1✔
746

747
        for (ImmudbProto.Entry entry : entries.getEntriesList()) {
1✔
748
            result.add(Entry.valueOf(entry));
1✔
749
        }
1✔
750

751
        return result;
1✔
752
    }
753

754
    /**
755
     * @param key the keys to look for
756
     * @return the latest entry associated to the provided key. Equivalent to
757
     *         {@link #get(String) get} but with additional server-provided proof validation.
758
     *         Returned object can be cast to {@link VerifiableEntry} instance.
759
     * @throws KeyNotFoundException  if the key is not found
760
     * @throws VerificationException if proof validation fails
761
     */
762
    public Entry verifiedGet(String key) throws KeyNotFoundException, VerificationException {
763
        return verifiedGetAtTx(key, 0);
1✔
764
    }
765

766
    /**
767
     * @param key the keys to look for
768
     * @return the latest entry associated to the provided key. Equivalent to
769
     *         {@link #get(byte[]) get} but with additional
770
     *         server-provided proof validation.
771
     *         Returned object can be cast to {@link VerifiableEntry} instance.
772
     * @throws KeyNotFoundException  if the key is not found
773
     * @throws VerificationException if proof validation fails
774
     */
775
    public Entry verifiedGet(byte[] key) throws KeyNotFoundException, VerificationException {
776
        return verifiedGetAtTx(key, 0);
1✔
777
    }
778

779
    /**
780
     * @param key the key to look for
781
     * @param tx  the transaction at which the associated entry is expected to be
782
     * @return the entry associated to the provided key and transaction. Equivalent
783
     *         to {@link #getAtTx(String, long) getAtTx} but with additional
784
     *         server-provided proof validation.
785
     *         Returned object can be cast to {@link VerifiableEntry} instance.
786
     * @throws KeyNotFoundException  if the key is not found
787
     * @throws VerificationException if proof validation fails
788
     */
789
    public Entry verifiedGetAtTx(String key, long tx) throws KeyNotFoundException, VerificationException {
790
        return verifiedGetAtTx(Utils.toByteArray(key), tx);
1✔
791
    }
792

793
    /**
794
     * @param key the key to look for
795
     * @param tx  the transaction at which the associated entry is expected to be
796
     * @return the entry associated to the provided key and transaction. Equivalent
797
     *         to {@link #getAtTx(byte[], long) getAtTx} but with additional
798
     *         server-provided proof validation.
799
     *         Returned object can be cast to {@link VerifiableEntry} instance.
800
     * @throws KeyNotFoundException  if the key is not found
801
     * @throws VerificationException if proof validation fails
802
     */
803
    public synchronized Entry verifiedGetAtTx(byte[] key, long tx) throws KeyNotFoundException, VerificationException {
804
        final ImmuState state = state();
1✔
805

806
        final ImmudbProto.KeyRequest keyReq = ImmudbProto.KeyRequest.newBuilder()
1✔
807
                .setKey(Utils.toByteString(key))
1✔
808
                .setAtTx(tx)
1✔
809
                .build();
1✔
810

811
        return verifiedGet(keyReq, state);
1✔
812
    }
813

814
    /**
815
     * This method can be used to avoid additional latency while the server
816
     * waits for the indexer to finish indexing but still guarantees that
817
     * specific portion of the database history has already been indexed.
818
     * 
819
     * @param key the key to look for
820
     * @param tx  the lowest transaction from which the associated entry should be
821
     *            retrieved
822
     * @return the latest indexed entry associated the to provided key. Ensuring the
823
     *         indexing has already be completed up to the specified transaction.
824
     *         Equivalent to {@link #getSinceTx(String, long) getSinceTx} but with additional
825
     *         server-provided proof validation.
826
     *         Returned object can be cast to {@link VerifiableEntry} instance.
827
     * @throws KeyNotFoundException  if the key is not found
828
     * @throws VerificationException if proof validation fails
829
     */
830
    public Entry verifiedGetSinceTx(String key, long tx) throws KeyNotFoundException, VerificationException {
UNCOV
831
        return verifiedGetSinceTx(Utils.toByteArray(key), tx);
×
832
    }
833

834
    /**
835
     * This method can be used to avoid additional latency while the server
836
     * waits for the indexer to finish indexing but still guarantees that
837
     * specific portion of the database history has already been indexed.
838
     * 
839
     * @param key the key to look for
840
     * @param tx  the lowest transaction from which the associated entry should be
841
     *            retrieved
842
     * @return the latest indexed entry associated the to provided key. Ensuring the
843
     *         indexing has already be completed up to the specified transaction.
844
     *         Equivalent to {@link #getSinceTx(byte[], long) getSinceTx} but with additional
845
     *         server-provided proof validation.
846
     *         Returned object can be cast to {@link VerifiableEntry} instance.
847
     * @throws KeyNotFoundException  if the key is not found
848
     * @throws VerificationException if proof validation fails
849
     */
850
    public synchronized Entry verifiedGetSinceTx(byte[] key, long tx)
851
            throws KeyNotFoundException, VerificationException {
852

853
        final ImmuState state = state();
1✔
854

855
        final ImmudbProto.KeyRequest keyReq = ImmudbProto.KeyRequest.newBuilder()
1✔
856
                .setKey(Utils.toByteString(key))
1✔
857
                .setSinceTx(tx)
1✔
858
                .build();
1✔
859

UNCOV
860
        return verifiedGet(keyReq, state);
×
861
    }
862

863
    /**
864
     * @param key the key to look for
865
     * @param rev the specific revision
866
     * @return the specific revision for given key. Equivalent to
867
     *         {@link #getAtRevision(String, long) getAtRevision} but with
868
     *         additional server-provided proof validation.
869
     *         Returned object can be cast to {@link VerifiableEntry} instance.
870
     * @throws KeyNotFoundException  if the key is not found
871
     * @throws VerificationException if proof validation fails
872
     */
873
    public Entry verifiedGetAtRevision(String key, long rev) throws KeyNotFoundException, VerificationException {
874
        return verifiedGetAtRevision(Utils.toByteArray(key), rev);
1✔
875
    }
876

877
    /**
878
     * @param key the key to look for
879
     * @param rev the specific revision
880
     * @return the specific revision for given key. Equivalent to
881
     *         {@link #getAtRevision(byte[], long) getAtRevision} but with
882
     *         additional server-provided proof validation.
883
     *         Returned object can be cast to {@link VerifiableEntry} instance.
884
     * @throws KeyNotFoundException  if the key is not found
885
     * @throws VerificationException if proof validation fails
886
     */
887
    public synchronized Entry verifiedGetAtRevision(byte[] key, long rev)
888
            throws KeyNotFoundException, VerificationException {
889

890
        final ImmuState state = state();
1✔
891

892
        final ImmudbProto.KeyRequest keyReq = ImmudbProto.KeyRequest.newBuilder()
1✔
893
                .setKey(Utils.toByteString(key))
1✔
894
                .setAtRevision(rev)
1✔
895
                .build();
1✔
896

897
        return verifiedGet(keyReq, state);
1✔
898
    }
899

900
    private Entry verifiedGet(ImmudbProto.KeyRequest keyReq, ImmuState state)
901
            throws KeyNotFoundException, VerificationException {
902
        final ImmudbProto.VerifiableGetRequest vGetReq = ImmudbProto.VerifiableGetRequest.newBuilder()
1✔
903
                .setKeyRequest(keyReq)
1✔
904
                .setProveSinceTx(state.getTxId())
1✔
905
                .build();
1✔
906

907
        final ImmudbProto.VerifiableEntry vEntry;
908

909
        try {
910
            vEntry = blockingStub.verifiableGet(vGetReq);
1✔
911
        } catch (StatusRuntimeException e) {
1✔
912
            if (e.getMessage().contains("key not found")) {
1✔
913
                throw new KeyNotFoundException();
1✔
914
            }
915

916
            throw e;
1✔
917
        }
1✔
918

919
        final InclusionProof inclusionProof = InclusionProof.valueOf(vEntry.getInclusionProof());
1✔
920
        final DualProof dualProof = DualProof.valueOf(vEntry.getVerifiableTx().getDualProof());
1✔
921

922
        byte[] eh;
923
        long sourceId, targetId;
924
        byte[] sourceAlh;
925
        byte[] targetAlh;
926

927
        final Entry entry = Entry.valueOf(vEntry.getEntry());
1✔
928

929
        if (entry.getReferenceBy() == null && !Arrays.equals(keyReq.getKey().toByteArray(), entry.getKey())) {
1✔
UNCOV
930
            throw new VerificationException("Data is corrupted: entry does not belong to specified key");
×
931
        }
932

933
        if (entry.getReferenceBy() != null
1✔
UNCOV
934
                && !Arrays.equals(keyReq.getKey().toByteArray(), entry.getReferenceBy().getKey())) {
×
UNCOV
935
            throw new VerificationException("Data is corrupted: entry does not belong to specified key");
×
936
        }
937

938
        if (entry.getMetadata() != null && entry.getMetadata().deleted()) {
1✔
UNCOV
939
            throw new VerificationException("Data is corrupted: entry is marked as deleted");
×
940
        }
941

942
        if (keyReq.getAtTx() != 0 && entry.getTx() != keyReq.getAtTx()) {
1✔
UNCOV
943
            throw new VerificationException("Data is corrupted: entry does not belong to specified tx");
×
944
        }
945

946
        if (state.getTxId() <= entry.getTx()) {
1✔
947
            final byte[] digest = vEntry.getVerifiableTx().getDualProof().getTargetTxHeader().getEH().toByteArray();
1✔
948
            eh = CryptoUtils.digestFrom(digest);
1✔
949

950
            sourceId = state.getTxId();
1✔
951
            sourceAlh = CryptoUtils.digestFrom(state.getTxHash());
1✔
952
            targetId = entry.getTx();
1✔
953
            targetAlh = dualProof.targetTxHeader.alh();
1✔
954
        } else {
1✔
955
            final byte[] digest = vEntry.getVerifiableTx().getDualProof().getSourceTxHeader().getEH().toByteArray();
1✔
956
            eh = CryptoUtils.digestFrom(digest);
1✔
957

958
            sourceId = entry.getTx();
1✔
959
            sourceAlh = dualProof.sourceTxHeader.alh();
1✔
960
            targetId = state.getTxId();
1✔
961
            targetAlh = CryptoUtils.digestFrom(state.getTxHash());
1✔
962
        }
963

964
        final byte[] kvDigest = entry.digestFor(vEntry.getVerifiableTx().getTx().getHeader().getVersion());
1✔
965

966
        if (!CryptoUtils.verifyInclusion(inclusionProof, kvDigest, eh)) {
1✔
UNCOV
967
            throw new VerificationException("Inclusion verification failed.");
×
968
        }
969

970
        if (state.getTxId() > 0) {
1✔
971
            if (!CryptoUtils.verifyDualProof(
1✔
972
                    dualProof,
973
                    sourceId,
974
                    targetId,
975
                    sourceAlh,
976
                    targetAlh)) {
UNCOV
977
                throw new VerificationException("Dual proof verification failed.");
×
978
            }
979
        }
980

981
        final ImmuState newState = new ImmuState(
1✔
982
                session.getDatabase(),
1✔
983
                targetId,
984
                targetAlh,
985
                vEntry.getVerifiableTx().getSignature().toByteArray());
1✔
986

987
        if (!newState.checkSignature(serverSigningKey)) {
1✔
UNCOV
988
            throw new VerificationException("State signature verification failed");
×
989
        }
990

991
        stateHolder.setState(newState);
1✔
992

993
        return VerifiableEntry.newBuilder()
1✔
994
                .withEntry(entry)
1✔
995
                .withInclusionProof(inclusionProof)
1✔
996
                .withVerifiableTx(VerifiableTx.valueOf(vEntry.getVerifiableTx()))
1✔
997
                .build();
1✔
998
    }
999

1000
    //
1001
    // ========== DELETE ==========
1002
    //
1003

1004
    /**
1005
     * Performs a logical deletion for key
1006
     * 
1007
     * @param key the key to delete
1008
     * @throws KeyNotFoundException if the key is not found
1009
     */
1010
    public TxHeader delete(String key) throws KeyNotFoundException {
1011
        return delete(Utils.toByteArray(key));
1✔
1012
    }
1013

1014
    /**
1015
     * Performs a logical deletion for key
1016
     * 
1017
     * @param key the key to delete
1018
     * @throws KeyNotFoundException if the key is not found
1019
     */
1020
    public synchronized TxHeader delete(byte[] key) throws KeyNotFoundException {
1021
        try {
1022
            final ImmudbProto.DeleteKeysRequest req = ImmudbProto.DeleteKeysRequest.newBuilder()
1✔
1023
                    .addKeys(Utils.toByteString(key))
1✔
1024
                    .build();
1✔
1025

1026
            return TxHeader.valueOf(blockingStub.delete(req));
1✔
1027
        } catch (StatusRuntimeException e) {
1✔
1028
            if (e.getMessage().contains("key not found")) {
1✔
1029
                throw new KeyNotFoundException();
1✔
1030
            }
1031

UNCOV
1032
            throw e;
×
1033
        }
1034
    }
1035

1036
    //
1037
    // ========== HISTORY ==========
1038
    //
1039

1040
    /**
1041
     * @param key    the key to look for
1042
     * @param offset the number of entries to be skipped
1043
     * @param desc   the order in which entries are returned
1044
     * @param limit  the maximum number of entries to be returned
1045
     * @return the list of entries associated to the provided key
1046
     * @throws KeyNotFoundException if the key is not found
1047
     */
1048
    public List<Entry> historyAll(String key, boolean desc, long offset, int limit) throws KeyNotFoundException {
1049
        return historyAll(Utils.toByteArray(key), desc, offset, limit);
1✔
1050
    }
1051

1052
    /**
1053
     * @param key    the key to look for
1054
     * @param desc   the order in which entries are returned
1055
     * @param offset the number of entries to be skipped
1056
     * @param limit  the maximum number of entries to be returned
1057
     * @return the list of entries associated to the provided key
1058
     * @throws KeyNotFoundException if the key is not found
1059
     */
1060
    public synchronized List<Entry> historyAll(byte[] key, boolean desc, long offset, int limit)
1061
            throws KeyNotFoundException {
1062
        try {
1063
            ImmudbProto.Entries entries = blockingStub.history(ImmudbProto.HistoryRequest.newBuilder()
1✔
1064
                    .setKey(Utils.toByteString(key))
1✔
1065
                    .setDesc(desc)
1✔
1066
                    .setOffset(offset)
1✔
1067
                    .setLimit(limit)
1✔
1068
                    .build());
1✔
1069

1070
            return buildList(entries);
1✔
1071
        } catch (StatusRuntimeException e) {
1✔
1072
            if (e.getMessage().contains("key not found")) {
1✔
1073
                throw new KeyNotFoundException();
1✔
1074
            }
1075

UNCOV
1076
            throw e;
×
1077
        }
1078
    }
1079

1080
    //
1081
    // ========== SCAN ==========
1082
    //
1083

1084
    /**
1085
     * @param prefix the prefix used to filter entries
1086
     * @return the list of entries with a maching prefix
1087
     */
1088
    public List<Entry> scanAll(String prefix) {
1089
        return scanAll(Utils.toByteArray(prefix));
1✔
1090
    }
1091

1092
    /**
1093
     * @param prefix the prefix used to filter entries
1094
     * @return the list of entries with a maching prefix
1095
     */
1096
    public List<Entry> scanAll(byte[] prefix) {
1097
        return scanAll(prefix, false, 0);
1✔
1098
    }
1099

1100
    /**
1101
     * @param prefix the prefix used to filter entries
1102
     * @param desc   the order in which entries are returned
1103
     * @param limit  the maximum number of entries to be returned
1104
     * @return the list of entries with a maching prefix
1105
     */
1106
    public List<Entry> scanAll(String prefix, boolean desc, long limit) {
UNCOV
1107
        return scanAll(Utils.toByteArray(prefix), null, desc, limit);
×
1108
    }
1109

1110
    /**
1111
     * @param prefix the prefix used to filter entries
1112
     * @param desc   the order in which entries are returned
1113
     * @param limit  the maximum number of entries to be returned
1114
     * @return the list of entries with a maching prefix
1115
     */
1116
    public List<Entry> scanAll(byte[] prefix, boolean desc, long limit) {
1117
        return scanAll(prefix, null, desc, limit);
1✔
1118
    }
1119

1120
    /**
1121
     * @param prefix  the prefix used to filter entries
1122
     * @param seekKey the initial key from which the scan begins
1123
     * @param desc    the order in which entries are returned
1124
     * @param limit   the maximum number of entries to be returned
1125
     * @return the list of entries with a maching prefix
1126
     */
1127
    public List<Entry> scanAll(byte[] prefix, byte[] seekKey, boolean desc, long limit) {
1128
        return scanAll(prefix, seekKey, null, desc, limit);
1✔
1129
    }
1130

1131
    /**
1132
     * @param prefix  the prefix used to filter entries
1133
     * @param seekKey the initial key from which the scan begins
1134
     * @param endKey  the final key at which the scan ends
1135
     * @param desc    the order in which entries are returned
1136
     * @param limit   the maximum number of entries to be returned
1137
     * @return the list of entries with a maching prefix
1138
     */
1139
    public List<Entry> scanAll(byte[] prefix, byte[] seekKey, byte[] endKey, boolean desc, long limit) {
1140
        return scanAll(prefix, seekKey, endKey, true, true, desc, limit);
1✔
1141
    }
1142

1143
    /**
1144
     * @param prefix        the prefix used to filter entries
1145
     * @param seekKey       the initial key from which the scan begins
1146
     * @param endKey        the final key at which the scan ends
1147
     * @param inclusiveSeek used to include/exclude the seekKey from the result
1148
     * @param inclusiveEnd  used to include/exclude the endKey from the result
1149
     * @param desc          the order in which entries are returned
1150
     * @param limit         the maximum number of entries to be returned
1151
     * @return the list of entries with a maching prefix
1152
     */
1153
    public synchronized List<Entry> scanAll(byte[] prefix, byte[] seekKey, byte[] endKey, boolean inclusiveSeek,
1154
            boolean inclusiveEnd,
1155
            boolean desc, long limit) {
1156
        final ImmudbProto.ScanRequest req = ScanRequest.newBuilder()
1✔
1157
                .setPrefix(Utils.toByteString(prefix))
1✔
1158
                .setSeekKey(Utils.toByteString(seekKey))
1✔
1159
                .setEndKey(Utils.toByteString(endKey))
1✔
1160
                .setInclusiveSeek(inclusiveSeek)
1✔
1161
                .setInclusiveEnd(inclusiveEnd)
1✔
1162
                .setDesc(desc)
1✔
1163
                .setLimit(limit)
1✔
1164
                .build();
1✔
1165

1166
        final ImmudbProto.Entries entries = blockingStub.scan(req);
1✔
1167
        return buildList(entries);
1✔
1168
    }
1169

1170
    //
1171
    // ========== SET ==========
1172
    //
1173

1174
    /**
1175
     * Commits a change of a value for a single key.
1176
     * 
1177
     * @param key   the key to set
1178
     * @param value the value to set
1179
     * @return the transaction header
1180
     */
1181
    public TxHeader set(String key, byte[] value) {
1182
        return set(Utils.toByteArray(key), value);
1✔
1183
    }
1184

1185
    /**
1186
     * Commits a change of a value for a single key.
1187
     * 
1188
     * @param key   the key to set
1189
     * @param value the value to set
1190
     * @return the transaction header
1191
     */
1192
    public synchronized TxHeader set(byte[] key, byte[] value) {
1193
        SetOptions options = SetOptions.newBuilder()
1✔
1194
                .withKey(key)
1✔
1195
                .withValue(value)
1✔
1196
                .build();
1✔
1197
        return set(options);
1✔
1198
    }
1199

1200
    /**
1201
     * Commits a change of a value for a single key.
1202
     *
1203
     * @param options single parameter for holding different set options
1204
     * @return the transaction header
1205
     */
1206
    public synchronized TxHeader set(SetOptions options) {
1207
        final ImmudbProto.KeyValue kv = ImmudbProto.KeyValue.newBuilder()
1✔
1208
                .setKey(Utils.toByteString(options.getKey()))
1✔
1209
                .setValue(Utils.toByteString(options.getValue()))
1✔
1210
                .build();
1✔
1211

1212
        List<ImmudbProto.Precondition> preconditions = mapPreconditions(options.getPreconditions());
1✔
1213

1214
        final ImmudbProto.SetRequest req = ImmudbProto.SetRequest.newBuilder()
1✔
1215
                .addKVs(kv)
1✔
1216
                .addAllPreconditions(preconditions)
1✔
1217
                .build();
1✔
1218
        ImmudbProto.TxHeader txHeader = callExecutorWrapper(() -> blockingStub.set(req));
1✔
1219
        return TxHeader.valueOf(txHeader);
1✔
1220
    }
1221

1222
    /**
1223
     * Commits multiple entries in a single transaction.
1224
     * 
1225
     * @param kvList the list of key-value pairs to set
1226
     * @return the transaction header
1227
     */
1228
    public synchronized TxHeader setAll(List<KVPair> kvList) {
1229
        final ImmudbProto.SetRequest.Builder reqBuilder = ImmudbProto.SetRequest.newBuilder();
1✔
1230

1231
        for (KVPair kv : kvList) {
1✔
1232
            ImmudbProto.KeyValue.Builder kvBuilder = ImmudbProto.KeyValue.newBuilder();
1✔
1233

1234
            kvBuilder.setKey(Utils.toByteString(kv.getKey()));
1✔
1235
            kvBuilder.setValue(Utils.toByteString(kv.getValue()));
1✔
1236

1237
            reqBuilder.addKVs(kvBuilder.build());
1✔
1238
        }
1✔
1239

1240
        return TxHeader.valueOf(blockingStub.set(reqBuilder.build()));
1✔
1241
    }
1242

1243
    /**
1244
     * Cceates a reference to another key's value.
1245
     * Note: references can only be created to non-reference keys.
1246
     * 
1247
     * @param key           the key for the reference
1248
     * @param referencedKey the referenced key
1249
     * @return the transaction header
1250
     */
1251
    public TxHeader setReference(String key, String referencedKey) {
UNCOV
1252
        return setReference(Utils.toByteArray(key), Utils.toByteArray(referencedKey));
×
1253
    }
1254

1255
    /**
1256
     * Cceates a reference to another key's value.
1257
     * Note: references can only be created to non-reference keys.
1258
     * 
1259
     * @param key           the key for the reference
1260
     * @param referencedKey the referenced key
1261
     * @return the transaction header
1262
     */
1263
    public TxHeader setReference(byte[] key, byte[] referencedKey) {
1264
        return setReference(key, referencedKey, 0);
1✔
1265
    }
1266

1267
    /**
1268
     * Cceates a reference to another key's value.
1269
     * Note: references can only be created to non-reference keys.
1270
     * 
1271
     * @param key           the key for the reference
1272
     * @param referencedKey the referenced key
1273
     * @param atTx          use a specific entry instead of the current value to
1274
     *                      bind the reference
1275
     * @return the transaction header
1276
     */
1277
    public TxHeader setReference(String key, String referencedKey, long atTx) {
UNCOV
1278
        return setReference(Utils.toByteArray(key), Utils.toByteArray(referencedKey), atTx);
×
1279
    }
1280

1281
    /**
1282
     * Cceates a reference to another key's value.
1283
     * Note: references can only be created to non-reference keys.
1284
     * 
1285
     * @param key           the key for the reference
1286
     * @param referencedKey the referenced key
1287
     * @param atTx          use a specific entry instead of the current value to
1288
     *                      bind the reference
1289
     * @return the transaction header
1290
     */
1291
    public synchronized TxHeader setReference(byte[] key, byte[] referencedKey, long atTx) {
1292
        final ImmudbProto.ReferenceRequest req = ImmudbProto.ReferenceRequest.newBuilder()
1✔
1293
                .setKey(Utils.toByteString(key))
1✔
1294
                .setReferencedKey(Utils.toByteString(referencedKey))
1✔
1295
                .setAtTx(atTx)
1✔
1296
                .setBoundRef(atTx > 0)
1✔
1297
                .build();
1✔
1298

1299
        return TxHeader.valueOf(blockingStub.setReference(req));
1✔
1300
    }
1301

1302
    /**
1303
     * Commits a change of a value for a single key.
1304
     * Equivalent to {@link #set(String, byte[]) set} but with additional
1305
     * server-provided proof validation.
1306
     * 
1307
     * @param key   the key to set
1308
     * @param value the value to set
1309
     * @return the transaction header
1310
     */
1311
    public TxHeader verifiedSet(String key, byte[] value) throws VerificationException {
1312
        return verifiedSet(Utils.toByteArray(key), value);
1✔
1313
    }
1314

1315
    /**
1316
     * Commits a change of a value for a single key.
1317
     * Equivalent to {@link #set(byte[], byte[]) set} but with additional
1318
     * server-provided proof validation.
1319
     *
1320
     * @param key   the key to set
1321
     * @param value the value to set
1322
     * @return the transaction header
1323
     */
1324
    public TxHeader verifiedSet(byte[] key, byte[] value) throws VerificationException {
1325
        SetOptions options = SetOptions.newBuilder()
1✔
1326
                .withKey(key)
1✔
1327
                .withValue(value)
1✔
1328
                .build();
1✔
1329
        return verifiedSet(options);
1✔
1330
    }
1331

1332
    /**
1333
     * Commits a change of a value for a single key.
1334
     * Equivalent to {@link #set(SetOptions) set} but with additional
1335
     * server-provided proof validation.
1336
     *
1337
     * @param options single parameter for holding different set options
1338
     * @return the transaction header
1339
     */
1340
    public synchronized TxHeader verifiedSet(SetOptions options)
1341
            throws VerificationException {
1342
        final ImmuState state = state();
1✔
1343

1344
        final ImmudbProto.KeyValue kv = ImmudbProto.KeyValue.newBuilder()
1✔
1345
                .setKey(Utils.toByteString(options.getKey()))
1✔
1346
                .setValue(Utils.toByteString(options.getValue()))
1✔
1347
                .build();
1✔
1348

1349
        List<ImmudbProto.Precondition> preconditions = mapPreconditions(options.getPreconditions());
1✔
1350
        ImmudbProto.SetRequest.Builder sendReqBuilder = ImmudbProto.SetRequest
1351
                .newBuilder()
1✔
1352
                .addKVs(kv)
1✔
1353
                .addAllPreconditions(preconditions);
1✔
1354

1355
        final ImmudbProto.VerifiableSetRequest vSetReq = ImmudbProto.VerifiableSetRequest.newBuilder()
1✔
1356
                .setSetRequest(sendReqBuilder.build())
1✔
1357
                .setProveSinceTx(state.getTxId())
1✔
1358
                .build();
1✔
1359

1360
        final ImmudbProto.VerifiableTx vtx = callExecutorWrapper(() -> blockingStub.verifiableSet(vSetReq));
1✔
1361
        final int ne = vtx.getTx().getHeader().getNentries();
1✔
1362

1363
        if (ne != 1 || vtx.getTx().getEntriesList().size() != 1) {
1✔
UNCOV
1364
            throw new VerificationException(
×
UNCOV
1365
                    String.format("Got back %d entries (in tx metadata) instead of 1.", ne));
×
1366
        }
1367

1368
        final Tx tx;
1369

1370
        try {
1371
            tx = Tx.valueOf(vtx.getTx());
1✔
UNCOV
1372
        } catch (Exception e) {
×
UNCOV
1373
            throw new VerificationException("Failed to extract the transaction.", e);
×
1374
        }
1✔
1375

1376
        final TxHeader txHeader = tx.getHeader();
1✔
1377

1378
        final Entry entry = Entry.valueOf(ImmudbProto.Entry.newBuilder()
1✔
1379
                .setKey(Utils.toByteString(options.getKey()))
1✔
1380
                .setValue(Utils.toByteString(options.getValue()))
1✔
1381
                .build());
1✔
1382

1383
        final InclusionProof inclusionProof = tx.proof(entry.getEncodedKey());
1✔
1384

1385
        if (!CryptoUtils.verifyInclusion(inclusionProof, entry.digestFor(txHeader.getVersion()), txHeader.getEh())) {
1✔
UNCOV
1386
            throw new VerificationException("Data is corrupted (verify inclusion failed)");
×
1387
        }
1388

1389
        final ImmuState newState = verifyDualProof(vtx, tx, state);
1✔
1390

1391
        if (!newState.checkSignature(serverSigningKey)) {
1✔
UNCOV
1392
            throw new VerificationException("State signature verification failed");
×
1393
        }
1394

1395
        stateHolder.setState(newState);
1✔
1396

1397
        return TxHeader.valueOf(vtx.getTx().getHeader());
1✔
1398
    }
1399

1400
    /**
1401
     * Cceates a reference to another key's value.
1402
     * Note: references can only be created to non-reference keys.
1403
     * 
1404
     * Equivalent to {@link #setReference(byte[], byte[]) set} but with additional
1405
     * server-provided proof validation.
1406
     * 
1407
     * @param key           the key for the reference
1408
     * @param referencedKey the referenced key
1409
     * @return the transaction header
1410
     */
1411
    public TxHeader verifiedSetReference(byte[] key, byte[] referencedKey) throws VerificationException {
UNCOV
1412
        return verifiedSetReference(key, referencedKey, 0);
×
1413
    }
1414

1415
    /**
1416
     * Cceates a reference to another key's value.
1417
     * Note: references can only be created to non-reference keys.
1418
     * 
1419
     * Equivalent to {@link #setReference(byte[], byte[], long) set} but with
1420
     * additional server-provided proof validation.
1421
     * 
1422
     * @param key           the key for the reference
1423
     * @param referencedKey the referenced key
1424
     * @param atTx          use a specific entry instead of the current value to
1425
     *                      bind the reference
1426
     * @return the transaction header
1427
     */
1428
    public synchronized TxHeader verifiedSetReference(byte[] key, byte[] referencedKey, long atTx)
1429
            throws VerificationException {
1430

1431
        final ImmuState state = state();
1✔
1432

1433
        final ImmudbProto.ReferenceRequest refReq = ImmudbProto.ReferenceRequest.newBuilder()
1✔
1434
                .setKey(Utils.toByteString(key))
1✔
1435
                .setReferencedKey(Utils.toByteString(referencedKey))
1✔
1436
                .setAtTx(atTx)
1✔
1437
                .setBoundRef(atTx > 0)
1✔
1438
                .build();
1✔
1439

1440
        final ImmudbProto.VerifiableReferenceRequest vRefReq = ImmudbProto.VerifiableReferenceRequest.newBuilder()
1✔
1441
                .setReferenceRequest(refReq)
1✔
1442
                .setProveSinceTx(state.getTxId())
1✔
1443
                .build();
1✔
1444

1445
        final ImmudbProto.VerifiableTx vtx = blockingStub.verifiableSetReference(vRefReq);
1✔
1446

1447
        final int vtxNentries = vtx.getTx().getHeader().getNentries();
1✔
1448
        if (vtxNentries != 1) {
1✔
UNCOV
1449
            throw new VerificationException(String.format("Data is corrupted (verifTx has %d Nentries instead of 1).",
×
UNCOV
1450
                    vtxNentries));
×
1451
        }
1452

1453
        final Tx tx;
1454
        try {
1455
            tx = Tx.valueOf(vtx.getTx());
1✔
UNCOV
1456
        } catch (Exception e) {
×
UNCOV
1457
            throw new VerificationException("Failed to extract the transaction.", e);
×
1458
        }
1✔
1459

1460
        final TxHeader txHeader = tx.getHeader();
1✔
1461

1462
        final Entry entry = Entry.valueOf(ImmudbProto.Entry.newBuilder()
1✔
1463
                .setKey(Utils.toByteString(referencedKey))
1✔
1464
                .setReferencedBy(ImmudbProto.Reference.newBuilder()
1✔
1465
                        .setKey(Utils.toByteString(key))
1✔
1466
                        .setAtTx(atTx)
1✔
1467
                        .build())
1✔
1468
                .build());
1✔
1469

1470
        final InclusionProof inclusionProof = tx.proof(entry.getEncodedKey());
1✔
1471

1472
        if (!CryptoUtils.verifyInclusion(inclusionProof, entry.digestFor(txHeader.getVersion()), txHeader.getEh())) {
1✔
UNCOV
1473
            throw new VerificationException("Data is corrupted (inclusion verification failed).");
×
1474
        }
1475

1476
        if (Arrays.equals(txHeader.getEh(),
1✔
1477
                CryptoUtils.digestFrom(vtx.getDualProof().getTargetTxHeader().getEH().toByteArray()))) {
1✔
1478
            throw new VerificationException("Data is corrupted (different digests).");
1✔
1479
        }
1480

UNCOV
1481
        final ImmuState newState = verifyDualProof(vtx, tx, state);
×
1482

UNCOV
1483
        if (!newState.checkSignature(serverSigningKey)) {
×
UNCOV
1484
            throw new VerificationException("State signature verification failed");
×
1485
        }
1486

UNCOV
1487
        stateHolder.setState(newState);
×
1488

UNCOV
1489
        return TxHeader.valueOf(vtx.getTx().getHeader());
×
1490
    }
1491

1492
    private ImmuState verifyDualProof(ImmudbProto.VerifiableTx vtx, Tx tx, ImmuState state)
1493
            throws VerificationException {
1494

1495
        final long sourceId = state.getTxId();
1✔
1496
        final long targetId = tx.getHeader().getId();
1✔
1497
        final byte[] sourceAlh = CryptoUtils.digestFrom(state.getTxHash());
1✔
1498
        final byte[] targetAlh = tx.getHeader().alh();
1✔
1499

1500
        if (state.getTxId() > 0) {
1✔
1501
            if (!CryptoUtils.verifyDualProof(
1✔
1502
                    DualProof.valueOf(vtx.getDualProof()),
1✔
1503
                    sourceId,
1504
                    targetId,
1505
                    sourceAlh,
1506
                    targetAlh)) {
UNCOV
1507
                throw new VerificationException("Data is corrupted (dual proof verification failed).");
×
1508
            }
1509
        }
1510

1511
        return new ImmuState(state.getDatabase(), targetId, targetAlh, vtx.getSignature().getSignature().toByteArray());
1✔
1512
    }
1513

1514
    //
1515
    // ========== Z ==========
1516
    //
1517

1518
    /**
1519
     * ZAdd adds a new entry to sorted set. New entry is a reference to some other
1520
     * key's value with additional score used for ordering set members.
1521
     * 
1522
     * @param set   the sorted set
1523
     * @param key   the key to be assigned
1524
     * @param score the score to assign
1525
     * @return the transaction header
1526
     */
1527
    public TxHeader zAdd(String set, String key, double score) {
1528
        return zAdd(Utils.toByteArray(set), Utils.toByteArray(key), score);
1✔
1529
    }
1530

1531
    /**
1532
     * ZAdd adds a new entry to sorted set. New entry is a reference to some other
1533
     * key's value with additional score used for ordering set members.
1534
     * 
1535
     * @param set   the sorted set
1536
     * @param key   the key to be assigned
1537
     * @param score the score to assign
1538
     * @return the transaction header
1539
     */
1540
    public TxHeader zAdd(byte[] set, byte[] key, double score) {
1541
        return zAdd(set, key, 0, score);
1✔
1542
    }
1543

1544
    /**
1545
     * ZAdd adds a new entry to sorted set. New entry is a reference to some other
1546
     * key's value with additional score used for ordering set members.
1547
     * 
1548
     * @param set   the sorted set
1549
     * @param key   the key to be assigned
1550
     * @param atTx  use a specific entry instead of the current value of the key
1551
     * @param score the score to assign
1552
     * @return the transaction header
1553
     */
1554
    public synchronized TxHeader zAdd(byte[] set, byte[] key, long atTx, double score) {
1555
        ImmudbProto.ZAddRequest req = ImmudbProto.ZAddRequest.newBuilder()
1✔
1556
                .setSet(Utils.toByteString(set))
1✔
1557
                .setKey(Utils.toByteString(key))
1✔
1558
                .setAtTx(atTx)
1✔
1559
                .setScore(score)
1✔
1560
                .setBoundRef(atTx > 0)
1✔
1561
                .build();
1✔
1562

1563
        return TxHeader.valueOf(blockingStub.zAdd(req));
1✔
1564
    }
1565

1566
    /**
1567
     * ZAdd adds a new entry to sorted set. New entry is a reference to some other
1568
     * key's value with additional score used for ordering set members.
1569
     * 
1570
     * Equivalent to {@link #zAdd(String, String, double) zAdd} but with additional
1571
     * server-provided proof validation.
1572
     * 
1573
     * @param set   the sorted set
1574
     * @param key   the key to be assigned
1575
     * @param score the score to assign
1576
     * @return the transaction header
1577
     */
1578
    public TxHeader verifiedZAdd(String set, String key, double score) throws VerificationException {
1579
        return verifiedZAdd(Utils.toByteArray(set), Utils.toByteArray(key), score);
1✔
1580
    }
1581

1582
    /**
1583
     * ZAdd adds a new entry to sorted set. New entry is a reference to some other
1584
     * key's value with additional score used for ordering set members.
1585
     * 
1586
     * Equivalent to {@link #zAdd(byte[], byte[], double) zAdd} but with additional
1587
     * server-provided proof validation.
1588
     * 
1589
     * @param set   the sorted set
1590
     * @param key   the key to be assigned
1591
     * @param score the score to assign
1592
     * @return the transaction header
1593
     */
1594
    public TxHeader verifiedZAdd(byte[] set, byte[] key, double score) throws VerificationException {
1595
        return verifiedZAdd(set, key, 0, score);
1✔
1596
    }
1597

1598
    /**
1599
     * ZAdd adds a new entry to sorted set. New entry is a reference to some other
1600
     * key's value with additional score used for ordering set members.
1601
     * 
1602
     * Equivalent to {@link #zAdd(byte[], byte[], long, double) zAdd} but with
1603
     * additional server-provided proof validation.
1604
     * 
1605
     * @param set   the sorted set
1606
     * @param key   the key to be assigned
1607
     * @param atTx  use a specific entry instead of the current value of the key
1608
     * @param score the score to assign
1609
     * @return the transaction header
1610
     */
1611
    public synchronized TxHeader verifiedZAdd(byte[] set, byte[] key, long atTx, double score)
1612
            throws VerificationException {
1613

1614
        final ImmuState state = state();
1✔
1615

1616
        final ImmudbProto.ZAddRequest zAddReq = ImmudbProto.ZAddRequest.newBuilder()
1✔
1617
                .setSet(Utils.toByteString(set))
1✔
1618
                .setKey(Utils.toByteString(key))
1✔
1619
                .setAtTx(atTx)
1✔
1620
                .setBoundRef(atTx > 0)
1✔
1621
                .setScore(score)
1✔
1622
                .build();
1✔
1623

1624
        final ImmudbProto.VerifiableZAddRequest vZAddReq = ImmudbProto.VerifiableZAddRequest.newBuilder()
1✔
1625
                .setZAddRequest(zAddReq)
1✔
1626
                .setProveSinceTx(state.getTxId())
1✔
1627
                .build();
1✔
1628

1629
        final ImmudbProto.VerifiableTx vtx = blockingStub.verifiableZAdd(vZAddReq);
1✔
1630

1631
        if (vtx.getTx().getHeader().getNentries() != 1) {
1✔
UNCOV
1632
            throw new VerificationException("Data is corrupted.");
×
1633
        }
1634

1635
        final Tx tx;
1636
        try {
1637
            tx = Tx.valueOf(vtx.getTx());
1✔
UNCOV
1638
        } catch (Exception e) {
×
UNCOV
1639
            throw new VerificationException("Failed to extract the transaction.", e);
×
1640
        }
1✔
1641

1642
        final TxHeader txHeader = tx.getHeader();
1✔
1643

1644
        final ZEntry entry = ZEntry.valueOf(ImmudbProto.ZEntry.newBuilder()
1✔
1645
                .setSet(Utils.toByteString(set))
1✔
1646
                .setKey(Utils.toByteString(key))
1✔
1647
                .setAtTx(atTx)
1✔
1648
                .setScore(score)
1✔
1649
                .build());
1✔
1650

1651
        InclusionProof inclusionProof = tx.proof(entry.getEncodedKey());
1✔
1652

1653
        if (!CryptoUtils.verifyInclusion(inclusionProof, entry.digestFor(txHeader.getVersion()), txHeader.getEh())) {
1✔
UNCOV
1654
            throw new VerificationException("Data is corrupted (inclusion verification failed).");
×
1655
        }
1656

1657
        if (!Arrays.equals(txHeader.getEh(),
1✔
1658
                CryptoUtils.digestFrom(vtx.getDualProof().getTargetTxHeader().getEH().toByteArray()))) {
1✔
UNCOV
1659
            throw new VerificationException("Data is corrupted (different digests).");
×
1660
        }
1661

1662
        final ImmuState newState = verifyDualProof(vtx, tx, state);
1✔
1663

1664
        if (!newState.checkSignature(serverSigningKey)) {
1✔
UNCOV
1665
            throw new VerificationException("State signature verification failed");
×
1666
        }
1667

1668
        stateHolder.setState(newState);
1✔
1669

1670
        return TxHeader.valueOf(vtx.getTx().getHeader());
1✔
1671
    }
1672

1673
    /**
1674
     * Iterates over the elements of sorted set ordered by their score.
1675
     * 
1676
     * @param set the set to iterate
1677
     * @return the list of entries in the set
1678
     */
1679
    public List<ZEntry> zScanAll(String set) {
1680
        return zScanAll(set, false, 0);
1✔
1681
    }
1682

1683
    /**
1684
     * Iterates over the elements of sorted set ordered by their score.
1685
     * 
1686
     * @param set   the set to iterate
1687
     * @param desc  the order in which entries are returned
1688
     * @param limit the maximum number of entries to be returned
1689
     * @return the list of entries in the set
1690
     */
1691
    public List<ZEntry> zScanAll(String set, boolean desc, long limit) {
1692
        return pzScanAll(Utils.toByteArray(set), null, null, null, null, 0, true, desc, limit);
1✔
1693
    }
1694

1695
    /**
1696
     * Iterates over the elements of sorted set ordered by their score.
1697
     * 
1698
     * @param set      the set to iterate
1699
     * @param minScore the minimum score required for returned entries
1700
     * @param maxScore the maximum score required for returned entries
1701
     * @param desc     the order in which entries are returned
1702
     * @param limit    the maximum number of entries to be returned
1703
     * @return the list of entries in the set
1704
     */
1705
    public List<ZEntry> zScanAll(byte[] set, double minScore, double maxScore, boolean desc, long limit) {
UNCOV
1706
        return pzScanAll(set, minScore, maxScore, null, null, 0, true, desc, 0);
×
1707
    }
1708

1709
    /**
1710
     * Iterates over the elements of sorted set ordered by their score.
1711
     * 
1712
     * @param set           the set to iterate
1713
     * @param minScore      the minimum score required for returned entries
1714
     * @param maxScore      the maximum score required for returned entries
1715
     * @param seekScore     the initial score from which the scan begins
1716
     * @param seekKey       the initial key from which the scan begins
1717
     * @param seekAtTx      the initial transaction from which the scan begins
1718
     * @param inclusiveSeek used to include/exclude the seekScore/seekKey/seekAtTx
1719
     *                      from the result
1720
     * @param desc          the order in which entries are returned
1721
     * @param limit         the maximum number of entries to be returned
1722
     * @return the list of entries in the set
1723
     */
1724
    public List<ZEntry> zScanAll(byte[] set, double minScore, double maxScore, double seekScore, byte[] seekKey,
1725
            long seekAtTx, boolean inclusiveSeek, boolean desc, long limit) {
UNCOV
1726
        return pzScanAll(set, minScore, maxScore, seekScore, seekKey, seekAtTx, inclusiveSeek, desc, limit);
×
1727
    }
1728

1729
    private synchronized List<ZEntry> pzScanAll(byte[] set, Double minScore, Double maxScore, Double seekScore,
1730
            byte[] seekKey,
1731
            long seekAtTx, boolean inclusiveSeek, boolean desc, long limit) {
1732

1733
        final ImmudbProto.ZScanRequest.Builder reqBuilder = ImmudbProto.ZScanRequest.newBuilder();
1✔
1734

1735
        reqBuilder.setSet(Utils.toByteString(set))
1✔
1736
                .setSeekKey(Utils.toByteString(seekKey))
1✔
1737
                .setSeekAtTx(seekAtTx)
1✔
1738
                .setInclusiveSeek(inclusiveSeek)
1✔
1739
                .setDesc(desc)
1✔
1740
                .setLimit(limit);
1✔
1741

1742
        if (seekScore != null) {
1✔
UNCOV
1743
            reqBuilder.setSeekScore(seekScore);
×
1744
        }
1745

1746
        if (minScore != null) {
1✔
UNCOV
1747
            reqBuilder.setMinScore(Score.newBuilder().setScore(minScore).build());
×
1748
        }
1749

1750
        if (maxScore != null) {
1✔
UNCOV
1751
            reqBuilder.setMaxScore(Score.newBuilder().setScore(maxScore).build());
×
1752
        }
1753

1754
        final ImmudbProto.ZEntries zEntries = blockingStub.zScan(reqBuilder.build());
1✔
1755

1756
        return buildList(zEntries);
1✔
1757
    }
1758

1759
    /**
1760
     * Retrieves all entries (in a raw, unprocessed form) for given transaction.
1761
     * Note: In order to read keys and values, it is necessary to parse returned
1762
     * entries
1763
     * 
1764
     * @param txId the transaction identifier
1765
     * @return transaction data
1766
     * @throws TxNotFoundException
1767
     * @throws NoSuchAlgorithmException
1768
     */
1769
    public synchronized Tx txById(long txId) throws TxNotFoundException, NoSuchAlgorithmException {
1770
        try {
1771
            final ImmudbProto.Tx tx = blockingStub.txById(ImmudbProto.TxRequest.newBuilder().setTx(txId).build());
1✔
1772
            return Tx.valueOf(tx);
1✔
1773
        } catch (StatusRuntimeException e) {
1✔
1774
            if (e.getMessage().contains("tx not found")) {
1✔
1775
                throw new TxNotFoundException();
1✔
1776
            }
1777

UNCOV
1778
            throw e;
×
1779
        }
1780
    }
1781

1782
    /**
1783
     * Retrieves all entries (in a raw, unprocessed form) for given transaction.
1784
     * Note: In order to read keys and values, it is necessary to parse returned
1785
     * entries
1786
     * 
1787
     * Equivalent to {@link #txById(long) txById} but with
1788
     * additional server-provided proof validation.
1789
     * 
1790
     * @param txId the transaction identifier
1791
     * @return transaction data
1792
     * @throws TxNotFoundException
1793
     * @throws VerificationException
1794
     */
1795
    public synchronized Tx verifiedTxById(long txId) throws TxNotFoundException, VerificationException {
1796
        final ImmuState state = state();
1✔
1797

1798
        final ImmudbProto.VerifiableTxRequest vTxReq = ImmudbProto.VerifiableTxRequest.newBuilder()
1✔
1799
                .setTx(txId)
1✔
1800
                .setProveSinceTx(state.getTxId())
1✔
1801
                .build();
1✔
1802

1803
        final ImmudbProto.VerifiableTx vtx;
1804

1805
        try {
1806
            vtx = blockingStub.verifiableTxById(vTxReq);
1✔
1807
        } catch (StatusRuntimeException e) {
1✔
1808
            if (e.getMessage().contains("tx not found")) {
1✔
1809
                throw new TxNotFoundException();
1✔
1810
            }
1811

UNCOV
1812
            throw e;
×
1813
        }
1✔
1814

1815
        final DualProof dualProof = DualProof.valueOf(vtx.getDualProof());
1✔
1816

1817
        long sourceId;
1818
        long targetId;
1819
        byte[] sourceAlh;
1820
        byte[] targetAlh;
1821

1822
        if (state.getTxId() <= txId) {
1✔
1823
            sourceId = state.getTxId();
1✔
1824
            sourceAlh = CryptoUtils.digestFrom(state.getTxHash());
1✔
1825
            targetId = txId;
1✔
1826
            targetAlh = dualProof.targetTxHeader.alh();
1✔
1827
        } else {
UNCOV
1828
            sourceId = txId;
×
UNCOV
1829
            sourceAlh = dualProof.sourceTxHeader.alh();
×
UNCOV
1830
            targetId = state.getTxId();
×
UNCOV
1831
            targetAlh = CryptoUtils.digestFrom(state.getTxHash());
×
1832
        }
1833

1834
        if (state.getTxId() > 0) {
1✔
1835
            if (!CryptoUtils.verifyDualProof(
1✔
1836
                    DualProof.valueOf(vtx.getDualProof()),
1✔
1837
                    sourceId,
1838
                    targetId,
1839
                    sourceAlh,
1840
                    targetAlh)) {
UNCOV
1841
                throw new VerificationException("Data is corrupted (dual proof verification failed).");
×
1842
            }
1843
        }
1844

1845
        final Tx tx;
1846
        try {
1847
            tx = Tx.valueOf(vtx.getTx());
1✔
UNCOV
1848
        } catch (Exception e) {
×
UNCOV
1849
            throw new VerificationException("Failed to extract the transaction.", e);
×
1850
        }
1✔
1851

1852
        final ImmuState newState = new ImmuState(state.getDatabase(), targetId, targetAlh,
1✔
1853
                vtx.getSignature().getSignature().toByteArray());
1✔
1854

1855
        if (!newState.checkSignature(serverSigningKey)) {
1✔
UNCOV
1856
            throw new VerificationException("State signature verification failed");
×
1857
        }
1858

1859
        stateHolder.setState(newState);
1✔
1860

1861
        return tx;
1✔
1862
    }
1863

1864
    /**
1865
     * returns raw entries for a range of transactions.
1866
     * 
1867
     * @param initialTxId the initial transaction from which the scan begins
1868
     * @return the list of transactions that begin with the specified transaction
1869
     */
1870
    public synchronized List<Tx> txScanAll(long initialTxId) {
1871
        final ImmudbProto.TxScanRequest req = ImmudbProto.TxScanRequest.newBuilder().setInitialTx(initialTxId).build();
1✔
1872
        final ImmudbProto.TxList txList = blockingStub.txScan(req);
1✔
1873

1874
        return buildList(txList);
1✔
1875
    }
1876

1877
    /**
1878
     * returns raw entries for a range of transactions.
1879
     * 
1880
     * @param initialTxId the initial transaction from which the scan begins
1881
     * @param desc        the order in which transactions are returned
1882
     * @param limit       the maximum number of transactions to be returned
1883
     * @return the list of transactions that begin with the specified transaction
1884
     */
1885
    public synchronized List<Tx> txScanAll(long initialTxId, boolean desc, int limit) {
1886
        final ImmudbProto.TxScanRequest req = ImmudbProto.TxScanRequest
1887
                .newBuilder()
1✔
1888
                .setInitialTx(initialTxId)
1✔
1889
                .setDesc(desc)
1✔
1890
                .setLimit(limit)
1✔
1891
                .build();
1✔
1892

1893
        final ImmudbProto.TxList txList = blockingStub.txScan(req);
1✔
1894
        return buildList(txList);
1✔
1895
    }
1896

1897
    //
1898
    // ========== STREAMS ==========
1899
    //
1900

1901
    private StreamObserver<ImmudbProto.TxHeader> txHeaderStreamObserver(LatchHolder<ImmudbProto.TxHeader> latchHolder) {
1902
        return new StreamObserver<ImmudbProto.TxHeader>() {
1✔
1903
            @Override
1904
            public void onCompleted() {
1905
            }
1✔
1906

1907
            @Override
1908
            public void onError(Throwable cause) {
1909
                throw new RuntimeException(cause);
×
1910
            }
1911

1912
            @Override
1913
            public void onNext(ImmudbProto.TxHeader hdr) {
1914
                latchHolder.doneWith(hdr);
1✔
1915
            }
1✔
1916
        };
1917
    }
1918

1919
    private void chunkIt(byte[] bs, StreamObserver<Chunk> streamObserver) {
1920
        final ByteBuffer buf = ByteBuffer.allocate(chunkSize).order(ByteOrder.BIG_ENDIAN);
1✔
1921

1922
        buf.putLong(bs.length);
1✔
1923

1924
        int i = 0;
1✔
1925

1926
        while (i < bs.length) {
1✔
1927
            final int chunkContentLen = Math.min(bs.length, buf.remaining());
1✔
1928

1929
            buf.put(bs, i, chunkContentLen);
1✔
1930

1931
            buf.flip();
1✔
1932

1933
            byte[] chunkContent = new byte[buf.limit()];
1✔
1934
            buf.get(chunkContent);
1✔
1935

1936
            final Chunk chunk = Chunk.newBuilder().setContent(Utils.toByteString(chunkContent)).build();
1✔
1937

1938
            streamObserver.onNext(chunk);
1✔
1939

1940
            buf.clear();
1✔
1941

1942
            i += chunkContentLen;
1✔
1943
        }
1✔
1944
    }
1✔
1945

1946
    private byte[] dechunkIt(Iterator<Chunk> chunks) {
1947
        final Chunk firstChunk = chunks.next();
1✔
1948
        final byte[] firstChunkContent = firstChunk.getContent().toByteArray();
1✔
1949

1950
        if (firstChunkContent.length < Long.BYTES) {
1✔
UNCOV
1951
            throw new RuntimeException("invalid chunk");
×
1952
        }
1953

1954
        final ByteBuffer b = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.BIG_ENDIAN);
1✔
1955
        b.put(firstChunkContent, 0, Long.BYTES);
1✔
1956

1957
        int payloadSize = (int) b.getLong(0);
1✔
1958

1959
        final ByteBuffer buf = ByteBuffer.allocate(payloadSize);
1✔
1960
        buf.put(firstChunkContent, Long.BYTES, firstChunkContent.length - Long.BYTES);
1✔
1961

1962
        while (buf.position() < payloadSize) {
1✔
UNCOV
1963
            Chunk chunk = chunks.next();
×
1964
            buf.put(chunk.getContent().toByteArray());
×
UNCOV
1965
        }
×
1966

1967
        return buf.array();
1✔
1968
    }
1969

1970
    private Iterator<Entry> entryIterator(Iterator<Chunk> chunks) {
1971
        return new Iterator<Entry>() {
1✔
1972

1973
            @Override
1974
            public boolean hasNext() {
1975
                try {
1976
                    return chunks.hasNext();
1✔
1977
                } catch (StatusRuntimeException e) {
1✔
1978
                    if (e.getMessage().contains("key not found")) {
1✔
1979
                        return false;
1✔
1980
                    }
1981

UNCOV
1982
                    throw e;
×
1983
                }
1984
            }
1985

1986
            @Override
1987
            public Entry next() {
1988
                return new Entry(dechunkIt(chunks), dechunkIt(chunks));
1✔
1989
            }
1990

1991
        };
1992
    }
1993

1994
    private Iterator<ZEntry> zentryIterator(Iterator<Chunk> chunks) {
1995
        return new Iterator<ZEntry>() {
1✔
1996

1997
            @Override
1998
            public boolean hasNext() {
1999
                try {
2000
                    return chunks.hasNext();
1✔
UNCOV
2001
                } catch (StatusRuntimeException e) {
×
UNCOV
2002
                    if (e.getMessage().contains("key not found")) {
×
UNCOV
2003
                        return false;
×
2004
                    }
2005

UNCOV
2006
                    throw e;
×
2007
                }
2008
            }
2009

2010
            @Override
2011
            public ZEntry next() {
2012
                final byte[] set = dechunkIt(chunks);
1✔
2013
                final byte[] key = dechunkIt(chunks);
1✔
2014

2015
                final ByteBuffer b = ByteBuffer.allocate(Double.BYTES + Long.BYTES).order(ByteOrder.BIG_ENDIAN);
1✔
2016
                b.put(dechunkIt(chunks)); // score
1✔
2017
                b.put(dechunkIt(chunks)); // atTx
1✔
2018

2019
                double score = b.getDouble(0);
1✔
2020
                long atTx = b.getLong(Double.BYTES);
1✔
2021

2022
                final byte[] value = dechunkIt(chunks);
1✔
2023

2024
                final Entry entry = new Entry(key, value);
1✔
2025

2026
                return new ZEntry(set, key, score, atTx, entry);
1✔
2027
            }
2028

2029
        };
2030
    }
2031

2032
    //
2033
    // ========== STREAM SET ==========
2034
    //
2035

2036
    /**
2037
     * Commits a change of a value for a single key.
2038
     * 
2039
     * Equivalent to {@link #set(String, byte[]) set} but with using grpc streams to
2040
     * support larger values.
2041
     * 
2042
     * @param key   the key to set
2043
     * @param value the value to set
2044
     * @return the transaction header
2045
     * @throws InterruptedException
2046
     */
2047
    public TxHeader streamSet(String key, byte[] value) throws InterruptedException {
2048
        return streamSet(Utils.toByteArray(key), value);
1✔
2049
    }
2050

2051
    /**
2052
     * Commits a change of a value for a single key.
2053
     * 
2054
     * Equivalent to {@link #set(byte[], byte[]) set} but with using grpc streams to
2055
     * support larger values.
2056
     * 
2057
     * @param key   the key to set
2058
     * @param value the value to set
2059
     * @return the transaction header
2060
     * @throws InterruptedException
2061
     */
2062
    public synchronized TxHeader streamSet(byte[] key, byte[] value)
2063
            throws InterruptedException {
2064
        final LatchHolder<ImmudbProto.TxHeader> latchHolder = new LatchHolder<>();
1✔
2065
        final StreamObserver<Chunk> streamObserver = nonBlockingStub.streamSet(txHeaderStreamObserver(latchHolder));
1✔
2066

2067
        chunkIt(key, streamObserver);
1✔
2068
        chunkIt(value, streamObserver);
1✔
2069

2070
        streamObserver.onCompleted();
1✔
2071

2072
        return TxHeader.valueOf(latchHolder.awaitValue());
1✔
2073
    }
2074

2075
    /**
2076
     * Commits multiple entries in a single transaction.
2077
     *
2078
     * @param kvList the list of key-value pairs to set
2079
     * @return the transaction header
2080
     * @throws InterruptedException
2081
     */
2082
    public synchronized TxHeader streamSetAll(List<KVPair> kvList) throws InterruptedException {
2083
        final LatchHolder<ImmudbProto.TxHeader> latchHolder = new LatchHolder<>();
1✔
2084
        final StreamObserver<Chunk> streamObserver = nonBlockingStub.streamSet(txHeaderStreamObserver(latchHolder));
1✔
2085

2086
        for (KVPair kv : kvList) {
1✔
2087
            chunkIt(kv.getKey(), streamObserver);
1✔
2088
            chunkIt(kv.getValue(), streamObserver);
1✔
2089
        }
1✔
2090

2091
        streamObserver.onCompleted();
1✔
2092

2093
        return TxHeader.valueOf(latchHolder.awaitValue());
1✔
2094
    }
2095

2096
    //
2097
    // ========== STREAM GET ==========
2098
    //
2099

2100
    /**
2101
     * Equivalent to {@link #get(String) get} but with using grpc streams to
2102
     * support larger values.
2103
     * 
2104
     * @param key the key to look for
2105
     * @return the latest entry associated to the provided key
2106
     * @throws KeyNotFoundException if the key is not found
2107
     */
2108
    public Entry streamGet(String key) throws KeyNotFoundException {
2109
        return streamGet(Utils.toByteArray(key));
1✔
2110
    }
2111

2112
    /**
2113
     * Equivalent to {@link #get(byte[]) get} but with using grpc streams to
2114
     * support larger values.
2115
     * 
2116
     * @param key the key to look for
2117
     * @return the latest entry associated to the provided key
2118
     * @throws KeyNotFoundException if the key is not found
2119
     */
2120
    public synchronized Entry streamGet(byte[] key) throws KeyNotFoundException {
2121
        final ImmudbProto.KeyRequest req = ImmudbProto.KeyRequest.newBuilder()
1✔
2122
                .setKey(Utils.toByteString(key))
1✔
2123
                .build();
1✔
2124

2125
        try {
2126
            final Iterator<Chunk> chunks = blockingStub.streamGet(req);
1✔
2127
            return new Entry(dechunkIt(chunks), dechunkIt(chunks));
1✔
2128
        } catch (StatusRuntimeException e) {
1✔
2129
            if (e.getMessage().contains("key not found")) {
1✔
2130
                throw new KeyNotFoundException();
1✔
2131
            }
2132

UNCOV
2133
            throw e;
×
2134
        }
2135
    }
2136

2137
    //
2138
    // ========== STREAM SCAN ==========
2139
    //
2140

2141
    /**
2142
     * Similar to {@link #scanAll(String) scanAll} but with using grpc streams to
2143
     * support larger values.
2144
     * 
2145
     * @param prefix the prefix used to filter entries
2146
     * @return an iterator over entries with a maching prefix
2147
     */
2148
    public Iterator<Entry> scan(String prefix) {
2149
        return scan(Utils.toByteArray(prefix));
1✔
2150
    }
2151

2152
    /**
2153
     * Similar to {@link #scanAll(byte[]) scanAll} but with using grpc streams to
2154
     * support larger values.
2155
     * 
2156
     * @param prefix the prefix used to filter entries
2157
     * @return an iterator over entries with a maching prefix
2158
     */
2159
    public Iterator<Entry> scan(byte[] prefix) {
2160
        return scan(prefix, false, 0);
1✔
2161
    }
2162

2163
    /**
2164
     * Similar to {@link #scanAll(String, boolean, long) scanAll} but with using
2165
     * grpc streams to support larger values.
2166
     * 
2167
     * @param prefix the prefix used to filter entries
2168
     * @param desc   the order in which entries are returned
2169
     * @param limit  the maximum number of entries to be returned
2170
     * @return an iterator over entries with a maching prefix
2171
     */
2172
    public Iterator<Entry> scan(String prefix, boolean desc, long limit) {
2173
        return scan(Utils.toByteArray(prefix), desc, limit);
1✔
2174
    }
2175

2176
    /**
2177
     * Similar to {@link #scanAll(byte[], boolean, long) scanAll} but with using
2178
     * grpc streams to support larger values.
2179
     * 
2180
     * @param prefix the prefix used to filter entries
2181
     * @param desc   the order in which entries are returned
2182
     * @param limit  the maximum number of entries to be returned
2183
     * @return an iterator over entries with a maching prefix
2184
     */
2185
    public Iterator<Entry> scan(byte[] prefix, boolean desc, long limit) {
2186
        return scan(prefix, null, desc, limit);
1✔
2187
    }
2188

2189
    /**
2190
     * Similar to {@link #scanAll(byte[], byte[], boolean, long) scanAll} but with
2191
     * using grpc streams to support larger values.
2192
     * 
2193
     * @param prefix  the prefix used to filter entries
2194
     * @param seekKey the initial key from which the scan begins
2195
     * @param desc    the order in which entries are returned
2196
     * @param limit   the maximum number of entries to be returned
2197
     * @return an iterator over entries with a maching prefix
2198
     */
2199
    public Iterator<Entry> scan(byte[] prefix, byte[] seekKey, boolean desc, long limit) {
2200
        return scan(prefix, seekKey, null, desc, limit);
1✔
2201
    }
2202

2203
    /**
2204
     * Similar to {@link #scanAll(byte[], byte[], byte[], boolean, long) scanAll}
2205
     * but with using grpc streams to support larger values.
2206
     * 
2207
     * @param prefix  the prefix used to filter entries
2208
     * @param seekKey the initial key from which the scan begins
2209
     * @param endKey  the final key at which the scan ends
2210
     * @param desc    the order in which entries are returned
2211
     * @param limit   the maximum number of entries to be returned
2212
     * @return an iterator over entries with a maching prefix
2213
     */
2214
    public Iterator<Entry> scan(byte[] prefix, byte[] seekKey, byte[] endKey, boolean desc, long limit) {
2215
        return scan(prefix, seekKey, endKey, true, true, desc, limit);
1✔
2216
    }
2217

2218
    /**
2219
     * Similar to
2220
     * {@link #scanAll(byte[], byte[], byte[], boolean, boolean, boolean, long)
2221
     * scanAll} but with using grpc streams to support larger values.
2222
     * 
2223
     * @param prefix        the prefix used to filter entries
2224
     * @param seekKey       the initial key from which the scan begins
2225
     * @param endKey        the final key at which the scan ends
2226
     * @param inclusiveSeek used to include/exclude the seekKey from the result
2227
     * @param inclusiveEnd  used to include/exclude the endKey from the result
2228
     * @param desc          the order in which entries are returned
2229
     * @param limit         the maximum number of entries to be returned
2230
     * @return an iterator over entries with a maching prefix
2231
     */
2232
    public synchronized Iterator<Entry> scan(byte[] prefix, byte[] seekKey, byte[] endKey, boolean inclusiveSeek,
2233
            boolean inclusiveEnd,
2234
            boolean desc, long limit) {
2235

2236
        final ImmudbProto.ScanRequest req = ScanRequest.newBuilder()
1✔
2237
                .setPrefix(Utils.toByteString(prefix))
1✔
2238
                .setSeekKey(Utils.toByteString(seekKey))
1✔
2239
                .setEndKey(Utils.toByteString(endKey))
1✔
2240
                .setInclusiveSeek(inclusiveSeek)
1✔
2241
                .setInclusiveEnd(inclusiveEnd)
1✔
2242
                .setDesc(desc)
1✔
2243
                .setLimit(limit)
1✔
2244
                .build();
1✔
2245

2246
        final Iterator<Chunk> chunks = blockingStub.streamScan(req);
1✔
2247

2248
        return entryIterator(chunks);
1✔
2249
    }
2250

2251
    //
2252
    // ========== STREAM ZSCAN ==========
2253
    //
2254

2255
    /**
2256
     * Iterates over the elements of sorted set ordered by their score.
2257
     * 
2258
     * Similar to {@link #zScanAll(String) zScanAll} but with using grpc streams to
2259
     * support larger values.
2260
     * 
2261
     * @param set the set to iterate
2262
     * @return an iterator over entries in the set
2263
     */
2264
    public Iterator<ZEntry> zScan(String set) {
2265
        return zScan(set, false, 0);
1✔
2266
    }
2267

2268
    /**
2269
     * Iterates over the elements of sorted set ordered by their score.
2270
     * 
2271
     * Similar to {@link #zScanAll(String, boolean, long) zScanAll} but with using
2272
     * grpc streams to support larger values.
2273
     * 
2274
     * @param set   the set to iterate
2275
     * @param desc  the order in which entries are returned
2276
     * @param limit the maximum number of entries to be returned
2277
     * @return an iterator over entries in the set
2278
     */
2279
    public Iterator<ZEntry> zScan(String set, boolean desc, long limit) {
2280
        return pzScan(Utils.toByteArray(set), null, null, null, null, 0, true, desc, limit);
1✔
2281
    }
2282

2283
    /**
2284
     * Iterates over the elements of sorted set ordered by their score.
2285
     * 
2286
     * Similar to {@link #zScanAll(byte[], double, double, boolean, long) zScanAll}
2287
     * but with using grpc streams to support larger values.
2288
     * 
2289
     * @param set      the set to iterate
2290
     * @param minScore the minimum score required for returned entries
2291
     * @param maxScore the maximum score required for returned entries
2292
     * @param desc     the order in which entries are returned
2293
     * @param limit    the maximum number of entries to be returned
2294
     * @return an iterator over entries in the set
2295
     */
2296
    public Iterator<ZEntry> zScan(byte[] set, double minScore, double maxScore, boolean desc, long limit) {
UNCOV
2297
        return pzScan(set, minScore, maxScore, null, null, 0, true, desc, 0);
×
2298
    }
2299

2300
    /**
2301
     * Iterates over the elements of sorted set ordered by their score.
2302
     * 
2303
     * Similar to
2304
     * {@link #zScanAll(byte[], double, double, double, byte[], long, boolean, boolean, long)
2305
     * zScanAll} but with using grpc streams to support larger values.
2306
     * 
2307
     * @param set           the set to iterate
2308
     * @param minScore      the minimum score required for returned entries
2309
     * @param maxScore      the maximum score required for returned entries
2310
     * @param seekScore     the initial score from which the scan begins
2311
     * @param seekKey       the initial key from which the scan begins
2312
     * @param seekAtTx      the initial transaction from which the scan begins
2313
     * @param inclusiveSeek used to include/exclude the seekScore/seekKey/seekAtTx
2314
     *                      from the result
2315
     * @param desc          the order in which entries are returned
2316
     * @param limit         the maximum number of entries to be returned
2317
     * @return an iterator over entries in the set
2318
     */
2319
    public Iterator<ZEntry> zScan(byte[] set, double minScore, double maxScore, double seekScore, byte[] seekKey,
2320
            long seekAtTx, boolean inclusiveSeek, boolean desc, long limit) {
UNCOV
2321
        return pzScan(set, minScore, maxScore, seekScore, seekKey, seekAtTx, inclusiveSeek, desc, limit);
×
2322
    }
2323

2324
    private synchronized Iterator<ZEntry> pzScan(byte[] set, Double minScore, Double maxScore, Double seekScore,
2325
            byte[] seekKey,
2326
            long seekAtTx, boolean inclusiveSeek, boolean desc, long limit) {
2327

2328
        final ImmudbProto.ZScanRequest.Builder reqBuilder = ImmudbProto.ZScanRequest.newBuilder();
1✔
2329

2330
        reqBuilder.setSet(Utils.toByteString(set))
1✔
2331
                .setSeekKey(Utils.toByteString(seekKey))
1✔
2332
                .setSeekAtTx(seekAtTx)
1✔
2333
                .setInclusiveSeek(inclusiveSeek)
1✔
2334
                .setDesc(desc)
1✔
2335
                .setLimit(limit);
1✔
2336

2337
        if (seekScore != null) {
1✔
UNCOV
2338
            reqBuilder.setSeekScore(seekScore);
×
2339
        }
2340

2341
        if (minScore != null) {
1✔
UNCOV
2342
            reqBuilder.setMinScore(Score.newBuilder().setScore(minScore).build());
×
2343
        }
2344

2345
        if (maxScore != null) {
1✔
UNCOV
2346
            reqBuilder.setMaxScore(Score.newBuilder().setScore(maxScore).build());
×
2347
        }
2348

2349
        final Iterator<Chunk> chunks = blockingStub.streamZScan(reqBuilder.build());
1✔
2350

2351
        return zentryIterator(chunks);
1✔
2352
    }
2353

2354
    //
2355
    // ========== STREAM HISTORY ==========
2356
    //
2357

2358
    /**
2359
     * Similar to {@link #historyAll(String, boolean, long, int) historyAll} but
2360
     * with using grpc streams to support larger values.
2361
     * 
2362
     * @param key    the key to look for
2363
     * @param offset the number of entries to be skipped
2364
     * @param desc   the order in which entries are returned
2365
     * @param limit  the maximum number of entries to be returned
2366
     * @return an iterator over entries associated to the provided key
2367
     * @throws KeyNotFoundException if the key is not found
2368
     */
2369
    public Iterator<Entry> history(String key, boolean desc, long offset, int limit) throws KeyNotFoundException {
2370
        return history(Utils.toByteArray(key), desc, offset, limit);
1✔
2371
    }
2372

2373
    /**
2374
     * Similar to {@link #historyAll(byte[], boolean, long, int) historyAll} but
2375
     * with using grpc streams to support larger values.
2376
     * 
2377
     * @param key    the key to look for
2378
     * @param offset the number of entries to be skipped
2379
     * @param desc   the order in which entries are returned
2380
     * @param limit  the maximum number of entries to be returned
2381
     * @return an iterator over entries associated to the provided key
2382
     * @throws KeyNotFoundException if the key is not found
2383
     */
2384
    public synchronized Iterator<Entry> history(byte[] key, boolean desc, long offset, int limit)
2385
            throws KeyNotFoundException {
2386
        try {
2387
            ImmudbProto.HistoryRequest req = ImmudbProto.HistoryRequest.newBuilder()
1✔
2388
                    .setKey(Utils.toByteString(key))
1✔
2389
                    .setDesc(desc)
1✔
2390
                    .setOffset(offset)
1✔
2391
                    .setLimit(limit)
1✔
2392
                    .build();
1✔
2393

2394
            final Iterator<Chunk> chunks = blockingStub.streamHistory(req);
1✔
2395

2396
            return entryIterator(chunks);
1✔
UNCOV
2397
        } catch (StatusRuntimeException e) {
×
UNCOV
2398
            if (e.getMessage().contains("key not found")) {
×
UNCOV
2399
                throw new KeyNotFoundException();
×
2400
            }
2401

UNCOV
2402
            throw e;
×
2403
        }
2404
    }
2405

2406
    //
2407
    // ========== HEALTH ==========
2408
    //
2409

2410
    public boolean isConnected() {
UNCOV
2411
        return channel != null && channel.getState(false) == ConnectivityState.READY;
×
2412
    }
2413

2414
    public boolean isShutdown() {
2415
        return channel != null && channel.isShutdown();
1✔
2416
    }
2417

2418
    public synchronized boolean healthCheck() {
2419
        return blockingStub.serverInfo(ImmudbProto.ServerInfoRequest.getDefaultInstance()) != null;
1✔
2420
    }
2421

2422
    //
2423
    // ========== USER MGMT ==========
2424
    //
2425

2426
    /**
2427
     * This call requires Admin or SysAdmin permission level.
2428
     * 
2429
     * When called as a SysAdmin user, all users in the database are returned.
2430
     * When called as an Admin user, users for currently selected database are
2431
     * returned.
2432
     * 
2433
     * @return the list of database users.
2434
     */
2435
    public synchronized List<User> listUsers() {
2436
        final ImmudbProto.UserList userList = blockingStub.listUsers(Empty.getDefaultInstance());
1✔
2437

2438
        return userList.getUsersList()
1✔
2439
                .stream()
1✔
2440
                .map(u -> User.getBuilder()
1✔
2441
                        .setUser(u.getUser().toString(StandardCharsets.UTF_8))
1✔
2442
                        .setActive(u.getActive())
1✔
2443
                        .setCreatedAt(u.getCreatedat())
1✔
2444
                        .setCreatedBy(u.getCreatedby())
1✔
2445
                        .setPermissions(buildPermissions(u.getPermissionsList()))
1✔
2446
                        .build())
1✔
2447
                .collect(Collectors.toList());
1✔
2448
    }
2449

2450
    private List<Permission> buildPermissions(List<ImmudbProto.Permission> permissionsList) {
2451
        return permissionsList
1✔
2452
                .stream()
1✔
2453
                .map(p -> Permission.valueOfPermissionCode(p.getPermission()))
1✔
2454
                .collect(Collectors.toList());
1✔
2455
    }
2456

2457
    /**
2458
     * Creates a new user with given credentials and permission.
2459
     *
2460
     * Required user permission is SysAdmin or database Admin.
2461
     *
2462
     * SysAdmin user can create users with access to any database.
2463
     *
2464
     * Admin user can only create users with access to databases where
2465
     * the user has admin permissions.
2466
     * 
2467
     * @param username   the username
2468
     * @param password   the password
2469
     * @param permission the permission to set
2470
     * @param database   the database name
2471
     */
2472
    public synchronized void createUser(String username, String password, Permission permission, String database) {
2473
        final ImmudbProto.CreateUserRequest createUserRequest = ImmudbProto.CreateUserRequest.newBuilder()
1✔
2474
                .setUser(Utils.toByteString(username))
1✔
2475
                .setPassword(Utils.toByteString(password))
1✔
2476
                .setPermission(permission.permissionCode)
1✔
2477
                .setDatabase(database)
1✔
2478
                .build();
1✔
2479

2480
        // noinspection ResultOfMethodCallIgnored
2481
        blockingStub.createUser(createUserRequest);
1✔
2482
    }
1✔
2483

2484
    /**
2485
     * Activate/Deactivate user
2486
     * 
2487
     * @param username the username
2488
     * @param active   the user status
2489
     */
2490
    public synchronized void activateUser(String username, boolean active) {
2491
        final ImmudbProto.SetActiveUserRequest req = ImmudbProto.SetActiveUserRequest.newBuilder()
1✔
2492
                .setUsername(username)
1✔
2493
                .setActive(active)
1✔
2494
                .build();
1✔
2495

2496
        // noinspection ResultOfMethodCallIgnored
2497
        blockingStub.setActiveUser(req);
1✔
2498
    }
1✔
2499

2500
    /**
2501
     * Changes password for existing user.
2502
     * 
2503
     * This call requires Admin or SysAdmin permission level.
2504
     * The currentPassword argument is only necessary when changing SysAdmin user's
2505
     * password.
2506
     * 
2507
     * @param username        the username
2508
     * @param currentPassword the current password
2509
     * @param newPassword     the new password
2510
     */
2511
    public synchronized void changePassword(String username, String currentPassword, String newPassword) {
2512
        final ImmudbProto.ChangePasswordRequest changePasswordRequest = ImmudbProto.ChangePasswordRequest.newBuilder()
1✔
2513
                .setUser(Utils.toByteString(username))
1✔
2514
                .setOldPassword(Utils.toByteString(currentPassword))
1✔
2515
                .setNewPassword(Utils.toByteString(newPassword))
1✔
2516
                .build();
1✔
2517

2518
        // noinspection ResultOfMethodCallIgnored
2519
        blockingStub.changePassword(changePasswordRequest);
1✔
2520
    }
1✔
2521

2522
    /**
2523
     * Add a permission for an existing user.
2524
     * 
2525
     * @param username   the username
2526
     * @param database   the database name
2527
     * @param permission the permission to grant
2528
     */
2529
    public synchronized void grantPermission(String username, String database, Permission permission) {
2530
        final ImmudbProto.ChangePermissionRequest req = ImmudbProto.ChangePermissionRequest.newBuilder()
1✔
2531
                .setUsername(username)
1✔
2532
                .setAction(ImmudbProto.PermissionAction.GRANT)
1✔
2533
                .setDatabase(database)
1✔
2534
                .setPermission(permission.permissionCode)
1✔
2535
                .build();
1✔
2536

2537
        // noinspection ResultOfMethodCallIgnored
2538
        blockingStub.changePermission(req);
1✔
2539
    }
1✔
2540

2541
    /**
2542
     * Remove a permission for an existing user.
2543
     * 
2544
     * @param username   the username
2545
     * @param database   the database name
2546
     * @param permission the permission to revoke
2547
     */
2548
    public synchronized void revokePermission(String username, String database, Permission permission) {
2549
        final ImmudbProto.ChangePermissionRequest req = ImmudbProto.ChangePermissionRequest.newBuilder()
1✔
2550
                .setUsername(username)
1✔
2551
                .setAction(ImmudbProto.PermissionAction.REVOKE)
1✔
2552
                .setDatabase(database)
1✔
2553
                .setPermission(permission.permissionCode)
1✔
2554
                .build();
1✔
2555

2556
        // noinspection ResultOfMethodCallIgnored
2557
        blockingStub.changePermission(req);
1✔
2558
    }
1✔
2559

2560
    //
2561
    // ========== INDEX MGMT ==========
2562
    //
2563

2564
    /**
2565
     * Requests a flush operation from the database. This call requires SysAdmin or
2566
     * Admin permission to given database.
2567
     * 
2568
     * @param cleanupPercentage the amount of index nodes data in percent that will
2569
     *                          be scanned in order to free up unused disk space
2570
     */
2571
    public synchronized void flushIndex(float cleanupPercentage) {
2572
        ImmudbProto.FlushIndexRequest req = ImmudbProto.FlushIndexRequest.newBuilder()
1✔
2573
                .setCleanupPercentage(cleanupPercentage)
1✔
2574
                .setSynced(true)
1✔
2575
                .build();
1✔
2576

2577
        blockingStub.flushIndex(req);
1✔
2578
    }
1✔
2579

2580
    /**
2581
     * Perform full database compaction. This call requires SysAdmin or Admin
2582
     * permission to given database.
2583
     * 
2584
     * Note: Full compaction will greatly affect the performance of the database.
2585
     * It should also be called only when there's a minimal database activity,
2586
     * if full compaction collides with a read or write operation, it will be
2587
     * aborted and may require retry of the whole operation. For that reason it is
2588
     * preferred to periodically call FlushIndex with a small value of
2589
     * cleanupPercentage or set the cleanupPercentage database option.
2590
     */
2591
    public synchronized void compactIndex() {
UNCOV
2592
        blockingStub.compactIndex(Empty.getDefaultInstance());
×
UNCOV
2593
    }
×
2594

2595
    //
2596
    // ========== INTERNAL UTILS ==========
2597
    //
2598

2599
    private List<Entry> buildList(ImmudbProto.Entries entries) {
2600
        final List<Entry> result = new ArrayList<>(entries.getEntriesCount());
1✔
2601
        entries.getEntriesList()
1✔
2602
                .forEach(entry -> result.add(Entry.valueOf(entry)));
1✔
2603
        return result;
1✔
2604
    }
2605

2606
    private List<ZEntry> buildList(ImmudbProto.ZEntries entries) {
2607
        final List<ZEntry> result = new ArrayList<>(entries.getEntriesCount());
1✔
2608
        entries.getEntriesList()
1✔
2609
                .forEach(entry -> result.add(ZEntry.valueOf(entry)));
1✔
2610
        return result;
1✔
2611
    }
2612

2613
    private List<Tx> buildList(ImmudbProto.TxList txList) {
2614
        final List<Tx> result = new ArrayList<>(txList.getTxsCount());
1✔
2615
        txList.getTxsList().forEach(tx -> {
1✔
2616
            try {
2617
                result.add(Tx.valueOf(tx));
1✔
UNCOV
2618
            } catch (Exception e) {
×
UNCOV
2619
                e.printStackTrace();
×
2620
            }
1✔
2621
        });
1✔
2622
        return result;
1✔
2623
    }
2624

2625
    private List<ImmudbProto.Precondition> mapPreconditions(List<Precondition> preconditions) {
2626
        return preconditions.stream()
1✔
2627
                .map(Precondition::toProto)
1✔
2628
                .collect(Collectors.toList());
1✔
2629
    }
2630

2631
    private <OUT> OUT callExecutorWrapper(Supplier<OUT> supplier) {
2632
        try {
2633
            return supplier.get();
1✔
2634
        } catch (StatusRuntimeException cause) {
1✔
2635
            if (Status.Code.FAILED_PRECONDITION == cause.getStatus().getCode()) {
1✔
2636
                throw new FailedPreconditionException(cause.getMessage(), cause);
1✔
2637
            }
UNCOV
2638
            throw cause;
×
2639
        }
2640
    }
2641

2642
    //
2643
    // ========== BUILDER ==========
2644
    //
2645

2646
    public static class Builder {
2647

2648
        private String serverUrl;
2649

2650
        private int serverPort;
2651

2652
        private PublicKey serverSigningKey;
2653

2654
        private long keepAlivePeriod;
2655

2656
        private int chunkSize;
2657

2658
        private ImmuStateHolder stateHolder;
2659

2660
        private Builder() {
1✔
2661
            serverUrl = "localhost";
1✔
2662
            serverPort = 3322;
1✔
2663
            stateHolder = new SerializableImmuStateHolder();
1✔
2664
            keepAlivePeriod = 60 * 1000; // 1 minute
1✔
2665
            chunkSize = 64 * 1024; // 64 * 1024 64 KiB
1✔
2666
        }
1✔
2667

2668
        public ImmuClient build() {
2669
            return new ImmuClient(this);
1✔
2670
        }
2671

2672
        public String getServerUrl() {
2673
            return serverUrl;
1✔
2674
        }
2675

2676
        public Builder withServerUrl(String serverUrl) {
2677
            this.serverUrl = serverUrl;
1✔
2678
            return this;
1✔
2679
        }
2680

2681
        public int getServerPort() {
2682
            return serverPort;
1✔
2683
        }
2684

2685
        public Builder withServerPort(int serverPort) {
2686
            this.serverPort = serverPort;
1✔
2687
            return this;
1✔
2688
        }
2689

2690
        public PublicKey getServerSigningKey() {
2691
            return serverSigningKey;
1✔
2692
        }
2693

2694
        /**
2695
         * Specify the public key file name (a DER file) that will
2696
         * be used for verifying the state signature received from the server.
2697
         */
2698
        public Builder withServerSigningKey(String publicKeyFilename) throws Exception {
2699
            serverSigningKey = CryptoUtils.getDERPublicKey(publicKeyFilename);
1✔
2700
            return this;
1✔
2701
        }
2702

2703
        public long getKeepAlivePeriod() {
2704
            return keepAlivePeriod;
1✔
2705
        }
2706

2707
        public Builder withKeepAlivePeriod(long keepAlivePeriod) {
UNCOV
2708
            this.keepAlivePeriod = keepAlivePeriod;
×
UNCOV
2709
            return this;
×
2710
        }
2711

2712
        public ImmuStateHolder getStateHolder() {
2713
            return stateHolder;
1✔
2714
        }
2715

2716
        public Builder withStateHolder(ImmuStateHolder stateHolder) {
2717
            this.stateHolder = stateHolder;
1✔
2718
            return this;
1✔
2719
        }
2720

2721
        public Builder withChunkSize(int chunkSize) {
UNCOV
2722
            if (chunkSize < Long.BYTES) {
×
UNCOV
2723
                throw new RuntimeException("invalid chunk size");
×
2724
            }
2725

UNCOV
2726
            this.chunkSize = chunkSize;
×
UNCOV
2727
            return this;
×
2728
        }
2729

2730
        public int getChunkSize() {
2731
            return chunkSize;
1✔
2732
        }
2733
    }
2734

2735
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc