Repository navigation
Expand file tree
/
Copy pathUtils.java
More file actions
1351 lines (1269 loc) · 68.1 KB
/
Copy pathUtils.java
File metadata and controls
1351 lines (1269 loc) · 68.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package com.skyflow.utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.google.gson.JsonObject;
import com.skyflow.config.VaultConfig;
import com.skyflow.enums.Env;
import com.skyflow.enums.UpdateType;
import com.skyflow.errors.ErrorCode;
import com.skyflow.errors.ErrorMessage;
import com.skyflow.errors.SkyflowException;
import com.skyflow.generated.rest.core.ApiClientApiException;
import com.skyflow.generated.rest.core.ObjectMappers;
import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest;
import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest;
import com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest;
import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest;
import com.skyflow.generated.rest.resources.flowservice.requests.V1UpdateRequest;
import com.skyflow.generated.rest.types.FlowEnumUpdateType;
import com.skyflow.generated.rest.types.V1ColumnRedactions;
import com.skyflow.generated.rest.types.V1DeleteResponse;
import com.skyflow.generated.rest.types.V1DeleteResponseObject;
import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject;
import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse;
import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse;
import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject;
import com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject;
import com.skyflow.generated.rest.types.V1FlowTokenizeResponse;
import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject;
import com.skyflow.generated.rest.types.V1GetRequestData;
import com.skyflow.generated.rest.types.V1GetResponse;
import com.skyflow.generated.rest.types.V1InsertRecordData;
import com.skyflow.generated.rest.types.V1InsertResponse;
import com.skyflow.generated.rest.types.V1RecordResponseObject;
import com.skyflow.generated.rest.types.V1TokenGroupRedactions;
import com.skyflow.generated.rest.types.V1UniqueValue;
import com.skyflow.generated.rest.types.V1UpdateRecordData;
import com.skyflow.generated.rest.types.V1UpdateResponse;
import com.skyflow.generated.rest.types.V1Upsert;
import com.skyflow.logs.ErrorLogs;
import com.skyflow.utils.logger.LogUtil;
import com.skyflow.vault.data.BulkDeleteTokensRequest;
import com.skyflow.vault.data.BulkDeleteTokensResponse;
import com.skyflow.vault.data.BulkDeleteTokensResponseRecord;
import com.skyflow.vault.data.BulkDetokenizeRequest;
import com.skyflow.vault.data.BulkDetokenizeResponse;
import com.skyflow.vault.data.BulkDetokenizeResponseRecord;
import com.skyflow.vault.data.BulkInsertRequest;
import com.skyflow.vault.data.BulkInsertResponse;
import com.skyflow.vault.data.BulkInsertResponseRecord;
import com.skyflow.vault.data.BulkTokenizeRequestRecord;
import com.skyflow.vault.data.BulkTokenizeResponse;
import com.skyflow.vault.data.BulkTokenizeResponseRecord;
import com.skyflow.vault.data.ColumnRedactions;
import com.skyflow.vault.data.DeleteRequest;
import com.skyflow.vault.data.DeleteResponse;
import com.skyflow.vault.data.DeleteResponseRecord;
import com.skyflow.vault.data.DetokenizeResponseRecordMetadata;
import com.skyflow.vault.data.DetokenizeRequest;
import com.skyflow.vault.data.DetokenizeResponse;
import com.skyflow.vault.data.DetokenizeResponseRecord;
import com.skyflow.vault.data.ErrorRecord;
import com.skyflow.vault.data.GetRequest;
import com.skyflow.vault.data.GetRequestRecord;
import com.skyflow.vault.data.GetResponse;
import com.skyflow.vault.data.GetResponseRecord;
import com.skyflow.vault.data.InsertRequest;
import com.skyflow.vault.data.InsertRequestRecord;
import com.skyflow.vault.data.InsertResponse;
import com.skyflow.vault.data.InsertResponseRecord;
import com.skyflow.vault.data.Token;
import com.skyflow.vault.data.TokenGroupRedactions;
import com.skyflow.vault.data.TokenizeRequestRecord;
import com.skyflow.vault.data.UpdateRequest;
import com.skyflow.vault.data.UpdateRequestRecord;
import com.skyflow.vault.data.UpdateResponse;
import com.skyflow.vault.data.UpdateResponseRecord;
import com.skyflow.vault.data.UpsertOptions;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvException;
public final class Utils extends BaseUtils {
// Spellings the vault has used for the per-record status, in precedence order.
private static final String[] HTTP_CODE_KEYS = {"http_code", "httpCode", "statusCode"};
public static String getVaultUrl(String clusterId, Env env) {
// The 3-arg overload is inherited from common's BaseUtils, which keeps the older
// getVaultURL spelling (shared with v2), so it is qualified rather than renamed here.
return BaseUtils.getVaultURL(clusterId, env, Constants.VAULT_DOMAIN);
}
public static JsonObject getMetrics() {
JsonObject details = getCommonMetrics();
String sdkVersion = Constants.SDK_VERSION;
details.addProperty(Constants.SDK_METRIC_NAME_VERSION, Constants.SDK_METRIC_NAME_VERSION_PREFIX + sdkVersion);
return details;
}
public static String getEnvVaultUrl() throws SkyflowException {
try {
String vaultUrl = System.getenv("VAULT_URL");
if (vaultUrl == null) {
Dotenv dotenv = Dotenv.load();
vaultUrl = dotenv.get("VAULT_URL");
}
if (vaultUrl != null && vaultUrl.trim().isEmpty()) {
LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_URL.getLog());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultUrl.getMessage());
} else if (vaultUrl != null && !isValidUrl(vaultUrl)) {
LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog());
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage());
}
return vaultUrl;
} catch (DotenvException e) {
return null;
}
}
public static boolean isValidUrl(String url) {
URL parsedUrl;
try {
parsedUrl = new URL(url);
} catch (MalformedURLException e) {
return false;
}
if (!parsedUrl.getProtocol().equalsIgnoreCase("https")) {
return false;
} else {
return parsedUrl.getHost() != null && !parsedUrl.getHost().isEmpty();
}
}
// Mirrors the "present" test used by the request validators: null and blank both count as absent.
private static boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
private static V1Upsert toV1Upsert(UpsertOptions upsert) {
V1Upsert.Builder builder = V1Upsert.builder().uniqueColumns(upsert.getUniqueColumns());
// updateType is a String on the request; the legal values come from the wire enum itself
// so there is a single source of truth. Validations rejects anything that does not match.
String updateType = upsert.getUpdateType();
for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) {
if (type.toString().equalsIgnoreCase(updateType)) {
builder.updateType(type);
break;
}
}
return builder.build();
}
public static V1InsertRequest getInsertRequestBody(InsertRequest request, VaultConfig config) {
List<InsertRequestRecord> records = request.getRecords();
List<V1InsertRecordData> insertRecordDataList = new ArrayList<>();
// tableName and upsert must reach the wire at exactly one level: the vault rejects a body
// that carries either at both the request and the record level. validateTableAndUpsertPlacement
// has already forced the caller to pick one, so mirror that choice here rather than copying
// the request-level value down onto every record.
for (InsertRequestRecord record : records) {
V1InsertRecordData.Builder data = V1InsertRecordData.builder()
.data(record.getData());
// A blank record-level table name counts as absent, matching validateInsertRequest.
if (hasText(record.getTableName())) {
data.tableName(record.getTableName());
}
if (record.getTokens() != null && !record.getTokens().isEmpty()) {
data.tokens(record.getTokens());
}
UpsertOptions recordUpsert = record.getUpsert();
if (recordUpsert != null && recordUpsert.getUniqueColumns() != null
&& !recordUpsert.getUniqueColumns().isEmpty()) {
data.upsert(toV1Upsert(recordUpsert));
}
insertRecordDataList.add(data.build());
}
V1InsertRequest.Builder builder = V1InsertRequest.builder()
.vaultId(config.getVaultId())
.records(insertRecordDataList);
if (hasText(request.getTableName())) {
builder.tableName(request.getTableName());
}
UpsertOptions requestUpsert = request.getUpsert();
if (requestUpsert != null && requestUpsert.getUniqueColumns() != null
&& !requestUpsert.getUniqueColumns().isEmpty()) {
builder.upsert(toV1Upsert(requestUpsert));
}
return builder.build();
}
private static String extractRequestId(Map<String, List<String>> headers) {
if (headers == null) return null;
List<String> ids = headers.get(BaseConstants.REQUEST_ID_HEADER_KEY);
return (ids == null || ids.isEmpty()) ? null : ids.get(0);
}
public static V1GetRequest getGetRequestBody(GetRequest request, VaultConfig config) {
V1GetRequest.Builder builder = V1GetRequest.builder().vaultId(config.getVaultId());
// Multi-table mode: Validations has already rejected the case where both this and the
// single-table fields below are set, so their presence/absence is mutually exclusive.
if (request.getRecords() != null && !request.getRecords().isEmpty()) {
List<V1GetRequestData> recordDataList = new ArrayList<>();
for (GetRequestRecord record : request.getRecords()) {
recordDataList.add(toV1GetRequestData(record));
}
return builder.records(recordDataList).build();
}
if (hasText(request.getTableName())) {
builder.tableName(request.getTableName());
}
if (request.getSkyflowIds() != null && !request.getSkyflowIds().isEmpty()) {
builder.skyflowIDs(request.getSkyflowIds());
}
if (request.getColumns() != null && !request.getColumns().isEmpty()) {
builder.columns(request.getColumns());
}
if (request.getColumnRedactions() != null && !request.getColumnRedactions().isEmpty()) {
builder.columnRedactions(toV1ColumnRedactionsList(request.getColumnRedactions()));
}
if (request.getUniqueValues() != null && !request.getUniqueValues().isEmpty()) {
builder.uniqueValues(toV1UniqueValueList(request.getUniqueValues()));
}
if (request.getLimit() != null) {
builder.limit(request.getLimit());
}
if (request.getOffset() != null) {
builder.offset(request.getOffset());
}
return builder.build();
}
private static V1GetRequestData toV1GetRequestData(GetRequestRecord record) {
V1GetRequestData.Builder data = V1GetRequestData.builder().tableName(record.getTableName());
if (record.getSkyflowIds() != null && !record.getSkyflowIds().isEmpty()) {
data.skyflowIDs(record.getSkyflowIds());
}
if (record.getColumns() != null && !record.getColumns().isEmpty()) {
data.columns(record.getColumns());
}
if (record.getColumnRedactions() != null && !record.getColumnRedactions().isEmpty()) {
data.columnRedactions(toV1ColumnRedactionsList(record.getColumnRedactions()));
}
if (record.getUniqueValues() != null && !record.getUniqueValues().isEmpty()) {
data.uniqueValues(toV1UniqueValueList(record.getUniqueValues()));
}
return data.build();
}
private static List<V1ColumnRedactions> toV1ColumnRedactionsList(List<ColumnRedactions> columnRedactions) {
List<V1ColumnRedactions> list = new ArrayList<>();
for (ColumnRedactions columnRedaction : columnRedactions) {
list.add(V1ColumnRedactions.builder()
.columnName(columnRedaction.getColumnName())
.redaction(columnRedaction.getRedaction())
.build());
}
return list;
}
private static List<V1UniqueValue> toV1UniqueValueList(List<Map<String, Object>> uniqueValues) {
List<V1UniqueValue> list = new ArrayList<>();
for (Map<String, Object> uniqueValue : uniqueValues) {
list.add(V1UniqueValue.builder().data(uniqueValue).build());
}
return list;
}
public static V1DeleteRequest getDeleteRequestBody(DeleteRequest request, VaultConfig config) {
V1DeleteRequest.Builder builder = V1DeleteRequest.builder()
.vaultId(config.getVaultId())
.tableName(request.getTableName());
if (request.getSkyflowIds() != null && !request.getSkyflowIds().isEmpty()) {
builder.skyflowIDs(request.getSkyflowIds());
}
if (request.getUniqueValues() != null && !request.getUniqueValues().isEmpty()) {
builder.uniqueValues(toV1UniqueValueList(request.getUniqueValues()));
}
return builder.build();
}
public static V1UpdateRequest getUpdateRequestBody(UpdateRequest request, VaultConfig config) {
List<V1UpdateRecordData> updateRecordDataList = new ArrayList<>();
for (UpdateRequestRecord record : request.getRecords()) {
V1UpdateRecordData.Builder data = V1UpdateRecordData.builder()
.skyflowId(record.getSkyflowId())
.data(record.getData());
if (record.getTokens() != null && !record.getTokens().isEmpty()) {
data.tokens(record.getTokens());
}
// A blank record-level table name counts as absent, matching validateInsertRequest's
// handling of the same table/record split.
if (hasText(record.getTableName())) {
data.tableName(record.getTableName());
}
updateRecordDataList.add(data.build());
}
V1UpdateRequest.Builder builder = V1UpdateRequest.builder()
.vaultId(config.getVaultId())
.tableName(request.getTableName())
.records(updateRecordDataList);
// Record-level updateType isn't wired here: the generated V1UpdateRecordData has no
// updateType setter yet even though the proto declares one (needs a client regeneration).
UpdateType requestUpdateType = request.getUpdateType();
if (requestUpdateType != null) {
builder.updateType(FlowEnumUpdateType.valueOf(requestUpdateType.name()));
}
return builder.build();
}
// ── Bulk (batched/concurrent) request-body builders ──────────────────────
// BulkInsertRequest is an InsertRequest, so the bulk body is built exactly the same way.
public static com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest getBulkInsertRequestBody(BulkInsertRequest request, VaultConfig config) {
return getInsertRequestBody(request, config);
}
public static V1FlowDetokenizeRequest getDetokenizeRequestBody(DetokenizeRequest request, String vaultId) {
V1FlowDetokenizeRequest.Builder builder = V1FlowDetokenizeRequest.builder()
.vaultId(vaultId)
.tokens(request.getTokens());
if (request.getTokenGroupRedactions() != null && !request.getTokenGroupRedactions().isEmpty()) {
List<V1TokenGroupRedactions> tokenGroupRedactionsList = new ArrayList<>();
for (TokenGroupRedactions tokenGroupRedactions : request.getTokenGroupRedactions()) {
tokenGroupRedactionsList.add(V1TokenGroupRedactions.builder()
.tokenGroupName(tokenGroupRedactions.getTokenGroupName())
.redaction(tokenGroupRedactions.getRedaction())
.build());
}
builder.tokenGroupRedactions(tokenGroupRedactionsList);
}
return builder.build();
}
public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokenizeRequest request, String vaultId) {
return getDetokenizeRequestBody(request, vaultId);
}
public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest getBulkDeleteTokensRequestBody(BulkDeleteTokensRequest request, String vaultId) {
return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder()
.vaultId(vaultId)
.tokens(request.getTokens())
.build();
}
public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest getBulkTokenizeRequestBody(
List<BulkTokenizeRequestRecord> records, String vaultId) {
List<V1FlowTokenizeRequestObject> dataList = new ArrayList<>();
for (BulkTokenizeRequestRecord record : records) {
dataList.add(buildTokenizeRequestObject(record));
}
return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder()
.vaultId(vaultId)
.data(dataList)
.build();
}
// ── Bulk batching, exception-handling and response-formatting helpers ────
public static List<List<V1InsertRecordData>> createBulkInsertBatches(List<V1InsertRecordData> records, int batchSize) {
List<List<V1InsertRecordData>> batches = new ArrayList<>();
for (int i = 0; i < records.size(); i += batchSize) {
batches.add(records.subList(i, Math.min(i + batchSize, records.size())));
}
return batches;
}
public static List<V1FlowDetokenizeRequest> createBulkDetokenizeBatches(V1FlowDetokenizeRequest request, int batchSize) {
List<V1FlowDetokenizeRequest> detokenizeRequests = new ArrayList<>();
List<String> tokens = request.getTokens().get();
for (int i = 0; i < tokens.size(); i += batchSize) {
List<String> batchTokens = tokens.subList(i, Math.min(i + batchSize, tokens.size()));
List<V1TokenGroupRedactions> tokenGroupRedactions = null;
if (request.getTokenGroupRedactions().isPresent() && !request.getTokenGroupRedactions().get().isEmpty()) {
tokenGroupRedactions = request.getTokenGroupRedactions().get();
}
V1FlowDetokenizeRequest batchRequest = V1FlowDetokenizeRequest.builder()
.vaultId(request.getVaultId())
.tokens(new ArrayList<>(batchTokens))
.tokenGroupRedactions(tokenGroupRedactions)
.build();
detokenizeRequests.add(batchRequest);
}
return detokenizeRequests;
}
public static List<com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest> createBulkDeleteTokensBatches(
com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request, int batchSize) {
List<com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest> batches = new ArrayList<>();
List<String> tokens = request.getTokens().get();
for (int i = 0; i < tokens.size(); i += batchSize) {
List<String> batchTokens = tokens.subList(i, Math.min(i + batchSize, tokens.size()));
com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batchRequest =
com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder()
.vaultId(request.getVaultId())
.tokens(new ArrayList<>(batchTokens))
.build();
batches.add(batchRequest);
}
return batches;
}
/**
* Splits the caller's records into batches, in order and without gaps.
*
* <p>A batch is closed early when the next record repeats a value already in it. The response
* carries no record identifier, so {@link #formatBulkTokenizeResponse} tells one record's rows
* from the next by watching the value change — which only works while values are distinct within
* a request. Batches stay contiguous, so a batch's records still occupy consecutive positions in
* the caller's list and their indexes follow from the batch's start.
*/
public static List<List<BulkTokenizeRequestRecord>> createBulkTokenizeBatches(
List<BulkTokenizeRequestRecord> records, int batchSize) {
List<List<BulkTokenizeRequestRecord>> batches = new ArrayList<>();
if (records == null || records.isEmpty()) return batches;
List<BulkTokenizeRequestRecord> current = new ArrayList<>();
Set<Object> valuesInBatch = new HashSet<>();
Set<String> valueKeysInBatch = new HashSet<>();
for (BulkTokenizeRequestRecord record : records) {
Object value = record == null ? null : record.getValue();
String valueKey = String.valueOf(value);
boolean repeatsValue = valuesInBatch.contains(value) || valueKeysInBatch.contains(valueKey);
if (!current.isEmpty() && (current.size() >= batchSize || repeatsValue)) {
batches.add(current);
current = new ArrayList<>();
valuesInBatch = new HashSet<>();
valueKeysInBatch = new HashSet<>();
}
current.add(record);
valuesInBatch.add(value);
valueKeysInBatch.add(valueKey);
}
batches.add(current);
return batches;
}
// Error bodies routinely carry a key whose value is explicitly null (e.g. "skyflowID": null on a
// failed record), so containsKey() is not enough to know a value is usable — read through these
// helpers. Anything that throws here masks the real server error with a parsing crash.
/** Value as a String, or null when the key is absent or explicitly null. */
private static String readString(Map<String, Object> recordMap, String key) {
Object value = recordMap.get(key);
return value == null ? null : value.toString();
}
/** First usable HTTP status among the known spellings, else {@code fallback}. */
private static int readHttpCode(Map<String, Object> recordMap, int fallback) {
for (String key : HTTP_CODE_KEYS) {
Object value = recordMap.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
}
if (value instanceof String) {
try {
return Integer.parseInt(((String) value).trim());
} catch (NumberFormatException ignored) {
// fall through to the next spelling
}
}
}
return fallback;
}
/**
* Error text from "error", else "message", else a placeholder. Never null: a null message on an
* error record would make it read as a success downstream, since that is how failures are counted.
*/
private static String readErrorMessage(Map<String, Object> recordMap) {
String error = readString(recordMap, "error");
if (error != null) {
return error;
}
String message = readString(recordMap, "message");
return message != null ? message : "Unknown error";
}
public static BulkInsertResponseRecord createInsertErrorRecord(Map<String, Object> recordMap, int indexNumber, String requestId) {
BulkInsertResponseRecord err = null;
if (recordMap != null) {
int code = readHttpCode(recordMap, 500);
String skyflowID = readString(recordMap, "skyflowID");
String tableName = readString(recordMap, "tableName");
String message = readErrorMessage(recordMap);
err = new BulkInsertResponseRecord(indexNumber, tableName, skyflowID, null, null, null, code, message, requestId);
}
return err;
}
public static BulkDetokenizeResponseRecord createDetokenizeErrorRecord(Map<String, Object> recordMap, int indexNumber, String requestId) {
BulkDetokenizeResponseRecord err = null;
if (recordMap != null) {
int code = readHttpCode(recordMap, 500);
// the failing token is echoed back so the caller can tell which one it was
String token = readString(recordMap, "token");
String tokenGroupName = readString(recordMap, "tokenGroupName");
String message = readErrorMessage(recordMap);
err = new BulkDetokenizeResponseRecord(indexNumber, token, null, tokenGroupName, null, code, message, requestId);
}
return err;
}
public static ErrorRecord createErrorRecord(Map<String, Object> recordMap, int indexNumber, String requestId) {
ErrorRecord err = null;
if (recordMap != null) {
int code = readHttpCode(recordMap, 500);
String message = readErrorMessage(recordMap);
err = new ErrorRecord(indexNumber, message, code, requestId);
}
return err;
}
// ── Unary "records"-shaped exception fallback ─────────────────────────────
//
// A unary call's own (only) record can fail outright — e.g. an invalid column on the sole
// record in an update/insert/get/delete request — and the vault reflects that as the overall
// HTTP status, so the generated client throws ApiClientApiException instead of returning a
// normal response body. When the exception body still has the familiar per-record shape
// ({"records": [...]} for insert/update/get/delete, {"response": [...]} for detokenize), the
// failure belongs on the response the same way a 200 partial-success does — not as a thrown
// exception. Each handler below returns null when the body doesn't match that shape, so the
// caller falls back to throwing a SkyflowException as before.
/** Record maps under {@code key} in an exception body, or null if the shape doesn't match. */
private static List<Map<String, Object>> extractExceptionRecords(ApiClientApiException apiException, String key) {
Object rawBody = apiException.body();
if (!(rawBody instanceof Map)) {
return null;
}
Object recordsField = ((Map<?, ?>) rawBody).get(key);
if (!(recordsField instanceof List)) {
return null;
}
List<Map<String, Object>> records = new ArrayList<>();
for (Object recordObj : (List<?>) recordsField) {
if (recordObj instanceof Map) {
//noinspection unchecked
records.add((Map<String, Object>) recordObj);
}
}
return records.isEmpty() ? null : records;
}
public static InsertResponse handleInsertRequestException(ApiClientApiException apiException) {
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
if (recordMaps == null) {
return null;
}
String requestId = extractRequestId(apiException.headers());
List<InsertResponseRecord> records = new ArrayList<>();
for (Map<String, Object> recordMap : recordMaps) {
records.add(new InsertResponseRecord(
readString(recordMap, "tableName"),
readString(recordMap, "skyflowID"),
null, null, null,
readHttpCode(recordMap, apiException.statusCode()),
readErrorMessage(recordMap),
requestId));
}
return new InsertResponse(records);
}
public static UpdateResponse handleUpdateRequestException(ApiClientApiException apiException) {
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
if (recordMaps == null) {
return null;
}
String requestId = extractRequestId(apiException.headers());
List<UpdateResponseRecord> records = new ArrayList<>();
for (Map<String, Object> recordMap : recordMaps) {
records.add(new UpdateResponseRecord(
readString(recordMap, "tableName"),
readString(recordMap, "skyflowID"),
null, null, null,
readHttpCode(recordMap, apiException.statusCode()),
readErrorMessage(recordMap),
requestId));
}
return new UpdateResponse(records);
}
public static GetResponse handleGetRequestException(ApiClientApiException apiException) {
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
if (recordMaps == null) {
return null;
}
String requestId = extractRequestId(apiException.headers());
List<GetResponseRecord> records = new ArrayList<>();
for (Map<String, Object> recordMap : recordMaps) {
records.add(new GetResponseRecord(
readString(recordMap, "tableName"),
readString(recordMap, "skyflowID"),
null, null, null,
readHttpCode(recordMap, apiException.statusCode()),
readErrorMessage(recordMap),
requestId));
}
return new GetResponse(records);
}
public static DeleteResponse handleDeleteRequestException(ApiClientApiException apiException) {
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "records");
if (recordMaps == null) {
return null;
}
String requestId = extractRequestId(apiException.headers());
List<DeleteResponseRecord> records = new ArrayList<>();
for (Map<String, Object> recordMap : recordMaps) {
records.add(new DeleteResponseRecord(
readString(recordMap, "skyflowID"),
readHttpCode(recordMap, apiException.statusCode()),
readErrorMessage(recordMap),
requestId));
}
return new DeleteResponse(records);
}
public static DetokenizeResponse handleDetokenizeRequestException(ApiClientApiException apiException) {
List<Map<String, Object>> recordMaps = extractExceptionRecords(apiException, "response");
if (recordMaps == null) {
return null;
}
String requestId = extractRequestId(apiException.headers());
List<DetokenizeResponseRecord> records = new ArrayList<>();
for (Map<String, Object> recordMap : recordMaps) {
records.add(new DetokenizeResponseRecord(
readString(recordMap, "token"),
null,
readString(recordMap, "tokenGroupName"),
null,
readHttpCode(recordMap, apiException.statusCode()),
readErrorMessage(recordMap),
requestId));
}
return new DetokenizeResponse(records);
}
// Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto
// the unified BulkInsertResponseRecord shape that bulk insert now returns.
public static List<BulkInsertResponseRecord> handleBulkInsertBatchException(
Throwable ex, List<V1InsertRecordData> batch, int batchNumber, int batchSize
) {
List<BulkInsertResponseRecord> allRecords = new ArrayList<>();
Throwable cause = ex.getCause();
if (cause instanceof ApiClientApiException) {
ApiClientApiException apiException = (ApiClientApiException) cause;
String requestId = extractRequestId(apiException.headers());
Object rawBody = apiException.body();
Map<String, Object> responseBody = (rawBody instanceof Map) ? (Map<String, Object>) rawBody : null;
int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0;
if (responseBody != null) {
if (responseBody.containsKey("records")) {
Object recordss = responseBody.get("records");
if (recordss instanceof List) {
List<?> recordsList = (List<?>) recordss;
for (Object record : recordsList) {
if (record instanceof Map) {
Map<String, Object> recordMap = (Map<String, Object>) record;
BulkInsertResponseRecord err = createInsertErrorRecord(recordMap, indexNumber, requestId);
allRecords.add(err);
indexNumber++;
}
}
}
} else if (responseBody.containsKey("error")) {
Object errField = responseBody.get("error");
Map<String, Object> recordMap = (errField instanceof Map) ? (Map<String, Object>) errField : null;
String fallbackMsg = (errField instanceof String) ? (String) errField : null;
for (int j = 0; j < batch.size(); j++) {
BulkInsertResponseRecord err = null;
if(recordMap != null){
err = createInsertErrorRecord(recordMap, indexNumber, requestId);
} else {
String errorMessage = null;
if (fallbackMsg != null){
errorMessage = fallbackMsg;
} else {
errorMessage = apiException.getMessage();
}
err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, apiException.statusCode(), errorMessage, requestId);
}
allRecords.add(err);
indexNumber++;
}
}
}
if (allRecords.isEmpty()) {
for (int j = 0; j < batch.size(); j++) {
allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId));
indexNumber++;
}
}
} else {
int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0;
for (int j = 0; j < batch.size(); j++) {
String message = null;
if (cause != null && cause.getMessage() != null){
message = cause.getMessage();
}
if (cause != null && cause.getLocalizedMessage() !=null) {
message = cause.getLocalizedMessage();
}
if (cause != null && cause.getCause() !=null) {
message = cause.getCause().toString();
}
if (message == null || message.isEmpty() || message.trim().isEmpty()){
message = ex.getMessage();
}
BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, 500, message, null);
allRecords.add(err);
indexNumber++;
}
}
return allRecords;
}
// Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto
// the unified BulkDetokenizeResponseRecord shape that bulk detokenize now returns.
public static List<BulkDetokenizeResponseRecord> handleBulkDetokenizeBatchException(
Throwable ex, V1FlowDetokenizeRequest batch, int batchNumber, int batchSize
) {
List<BulkDetokenizeResponseRecord> allRecords = new ArrayList<>();
Throwable cause = ex.getCause();
if (cause instanceof ApiClientApiException) {
ApiClientApiException apiException = (ApiClientApiException) cause;
String requestId = extractRequestId(apiException.headers());
Object rawBody = apiException.body();
Map<String, Object> responseBody = (rawBody instanceof Map) ? (Map<String, Object>) rawBody : null;
int indexNumber = batchNumber * batchSize;
int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0;
if (responseBody != null) {
if (responseBody.containsKey("response")) {
Object recordss = responseBody.get("response");
if (recordss instanceof List) {
List<?> recordsList = (List<?>) recordss;
for (Object record : recordsList) {
if (record instanceof Map) {
Map<String, Object> recordMap = (Map<String, Object>) record;
BulkDetokenizeResponseRecord err = createDetokenizeErrorRecord(recordMap, indexNumber, requestId);
allRecords.add(err);
indexNumber++;
}
}
}
} else if (responseBody.containsKey("error")) {
Object errField = responseBody.get("error");
Map<String, Object> recordMap = (errField instanceof Map) ? (Map<String, Object>) errField : null;
String fallbackMsg = (errField instanceof String) ? (String) errField : null;
for (int j = 0; j < tokenCount; j++) {
BulkDetokenizeResponseRecord err = null;
if (recordMap != null) {
err = createDetokenizeErrorRecord(recordMap, indexNumber, requestId);
} else {
String errorMessage = null;
if (fallbackMsg != null) {
errorMessage = fallbackMsg;
} else {
errorMessage = apiException.getMessage();
}
err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId);
}
allRecords.add(err);
indexNumber++;
}
}
}
if (allRecords.isEmpty()) {
for (int j = 0; j < tokenCount; j++) {
allRecords.add(new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId));
indexNumber++;
}
}
} else {
int indexNumber = batchNumber * batchSize;
String message = null;
if (cause != null && cause.getMessage() != null){
message = cause.getMessage();
}
if (cause != null && cause.getLocalizedMessage() !=null) {
message = cause.getLocalizedMessage();
}
if (cause != null && cause.getCause() !=null) {
message = cause.getCause().toString();
}
if (message == null || message.isEmpty() || message.trim().isEmpty()){
message = ex.getMessage();
}
for (int j = 0; j < batch.getTokens().get().size(); j++) {
BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, message, null);
allRecords.add(err);
indexNumber++;
}
}
return allRecords;
}
/**
* Best available description of a failure that never reached the API.
*
* <p>The generated client wraps transport failures as "Network error executing HTTP request",
* which says nothing about what actually went wrong, and the future wraps that again. Walk down
* to the innermost cause so the caller sees the real problem - for a mistyped cluster id that is
* {@code java.net.UnknownHostException: <host>: nodename nor servname provided, or not known}
* rather than the generic wrapper. Mirrors the order bulk insert and detokenize already use.
*/
private static String describeTransportFailure(Throwable ex, Throwable cause) {
String message = null;
if (cause != null && cause.getMessage() != null) {
message = cause.getMessage();
}
if (cause != null && cause.getLocalizedMessage() != null) {
message = cause.getLocalizedMessage();
}
if (cause != null && cause.getCause() != null) {
message = cause.getCause().toString();
}
if (message == null || message.trim().isEmpty()) {
message = ex.getMessage();
}
return message;
}
public static List<BulkDeleteTokensResponseRecord> handleBulkDeleteTokensBatchException(
Throwable ex,
com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch,
int batchNumber, int batchSize
) {
List<BulkDeleteTokensResponseRecord> errorRecords = new ArrayList<>();
List<String> batchTokens = (batch != null && batch.getTokens().isPresent())
? batch.getTokens().get() : new ArrayList<>();
int startIndex = batchNumber * batchSize;
Throwable cause = ex.getCause();
if (cause instanceof ApiClientApiException) {
ApiClientApiException apiException = (ApiClientApiException) cause;
String requestId = extractRequestId(apiException.headers());
Object rawBody = apiException.body();
Map<String, Object> responseBody = (rawBody instanceof Map) ? (Map<String, Object>) rawBody : null;
if (responseBody != null) {
if (responseBody.containsKey("tokens")) {
Object tokensList = responseBody.get("tokens");
if (tokensList instanceof List) {
List<?> recordsList = (List<?>) tokensList;
for (int position = 0; position < recordsList.size(); position++) {
Object record = recordsList.get(position);
if (record instanceof Map) {
Map<String, Object> recordMap = (Map<String, Object>) record;
errorRecords.add(createDeleteTokensErrorRecord(recordMap,
startIndex + position, tokenAt(batchTokens, position), requestId));
}
}
}
} else if (responseBody.containsKey("error")) {
Object errField = responseBody.get("error");
Map<String, Object> recordMap = (errField instanceof Map) ? (Map<String, Object>) errField : null;
String fallbackMsg = (errField instanceof String) ? (String) errField : null;
for (int position = 0; position < batchTokens.size(); position++) {
errorRecords.add((recordMap != null)
? createDeleteTokensErrorRecord(recordMap, startIndex + position,
tokenAt(batchTokens, position), requestId)
: new BulkDeleteTokensResponseRecord(
startIndex + position, tokenAt(batchTokens, position),
apiException.statusCode(),
fallbackMsg != null ? fallbackMsg : apiException.getMessage(),
requestId));
}
}
}
if (errorRecords.isEmpty()) {
for (int position = 0; position < batchTokens.size(); position++) {
errorRecords.add(new BulkDeleteTokensResponseRecord(
startIndex + position, tokenAt(batchTokens, position),
apiException.statusCode(), apiException.getMessage(), requestId));
}
}
} else {
// a transport-level failure never reached the API, so there is no id to report
String message = describeTransportFailure(ex, cause);
for (int position = 0; position < batchTokens.size(); position++) {
errorRecords.add(new BulkDeleteTokensResponseRecord(
startIndex + position, tokenAt(batchTokens, position), 500, message));
}
}
return errorRecords;
}
private static String tokenAt(List<String> tokens, int position) {
return (tokens != null && position < tokens.size()) ? tokens.get(position) : null;
}
private static BulkDeleteTokensResponseRecord createDeleteTokensErrorRecord(
Map<String, Object> recordMap, int index, String requestedToken, String requestId) {
// Read through the shared helpers rather than casting: recordMap holds deserialised JSON,
// so a status can arrive as Double or String depending on the parser, and a blind
// (Integer) cast would turn a real API error into a ClassCastException.
int code = readHttpCode(recordMap, 500);
String message = readErrorMessage(recordMap);
String token = readString(recordMap, "value");
if (token == null) {
token = requestedToken;
}
return new BulkDeleteTokensResponseRecord(index, token, code, message, requestId);
}
public static List<BulkTokenizeResponseRecord> handleBulkTokenizeBatchException(
Throwable ex, List<BulkTokenizeRequestRecord> batchRecords, int startIndex) {
String message;
int httpCode;
String requestId = null;
Throwable cause = ex.getCause();
if (cause instanceof ApiClientApiException) {
ApiClientApiException apiException = (ApiClientApiException) cause;
// a rejected request still describes each record in its body - prefer that detail over
// the bare status code, and only synthesise entries when there is nothing to read
List<BulkTokenizeResponseRecord> fromBody =
tokenizeRecordsFromErrorBody(apiException, batchRecords, startIndex);
if (fromBody != null) {
return fromBody;
}
httpCode = apiException.statusCode();
message = extractBatchErrorMessage(apiException);
requestId = extractRequestId(apiException.headers());
} else {
// a transport-level failure never reached the API, so there is no id to report
httpCode = 500;
message = describeTransportFailure(ex, cause);
}
// a batch-level failure fails every token group of every value in that batch
List<BulkTokenizeResponseRecord> errorRecords = new ArrayList<>();
if (batchRecords == null) return errorRecords;
for (int position = 0; position < batchRecords.size(); position++) {
BulkTokenizeRequestRecord requested = batchRecords.get(position);
int index = startIndex + position;
List<String> groupNames = requested.getTokenGroupNames();
if (groupNames == null || groupNames.isEmpty()) {
errorRecords.add(new BulkTokenizeResponseRecord(
index, requested.getValue(), null, null, httpCode, message, requestId));
} else {
for (String groupName : groupNames) {
errorRecords.add(new BulkTokenizeResponseRecord(
index, requested.getValue(), groupName, null, httpCode, message, requestId));
}
}
}
return errorRecords;
}
/**
* Rebuilds the response records from a rejected request's body.
*
* <p>The API answers a 4xx with the same {@code response} array it would have returned on
* success, one row per rejected (value, token group) carrying its own message - for example
* "Invalid request. BYOT token should contain one token group." Reading it keeps the caller's
* error identical whether the request was rejected outright or reported per record.
*
* @return null when the body is absent or in an unfamiliar shape, so the caller can fall back
* to summarising the batch by its status code
*/
private static List<BulkTokenizeResponseRecord> tokenizeRecordsFromErrorBody(
ApiClientApiException apiException,
List<BulkTokenizeRequestRecord> batchRecords,
int startIndex) {
Object rawBody = apiException.body();
if (!(rawBody instanceof Map) || !((Map<?, ?>) rawBody).containsKey("response")) {
return null;
}
try {
V1FlowTokenizeResponse parsed =
ObjectMappers.JSON_MAPPER.convertValue(rawBody, V1FlowTokenizeResponse.class);
if (!parsed.getResponse().isPresent() || parsed.getResponse().get().isEmpty()) {
return null;
}
return groupTokenizeRows(parsed.getResponse().get(), batchRecords, startIndex,
extractRequestId(apiException.headers()));
} catch (RuntimeException ignored) {
// body did not deserialise into the shape we know; let the caller summarise instead
return null;
}
}
/** Pulls the most specific message available from a failed batch response body. */
private static String extractBatchErrorMessage(ApiClientApiException apiException) {
Object rawBody = apiException.body();