001package gudusoft.gsqlparser.util; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TGSqlParser; 005import gudusoft.gsqlparser.util.json.JSON; 006 007import java.io.*; 008import java.util.ArrayList; 009import java.util.LinkedHashMap; 010import java.util.List; 011import java.util.Map; 012 013/** 014 * Splits large files into smaller chunks for parallel processing. 015 * 016 * <p>Supports two categories of input: 017 * <ul> 018 * <li><b>Plain SQL files</b> — split by statement boundaries using the SQL 019 * parser to find safe cut points.</li> 020 * <li><b>JSON metadata files</b> — split by format (sqldep/grabit, sqlflow, 021 * sqlflow-sharded), converting each into smaller self-contained JSON files 022 * that the re-reader can consume independently.</li> 023 * </ul> 024 * 025 * <h3>JSON format routing</h3> 026 * splitJsonFile() routes to the appropriate splitter based on 027 * {@code createdBy} and {@code format}: 028 * <ul> 029 * <li>{@code format: "sqlflow-sharded"} → splitSqlflowShardedJson()</li> 030 * <li>{@code createdBy} contains "sqldep" or "grabit" → splitSqldepGrabitJson()</li> 031 * <li>{@code createdBy} contains "sqlflow" → splitSqlflowJson()</li> 032 * </ul> 033 */ 034public class FileSplitter { 035 036 private static final int MAX_CONSECUTIVE_FAILURES = 3; 037 private static final int BUFFER_SIZE = 8192; 038 private static final Logger logger = LoggerFactory.getLogger(FileSplitter.class); 039 040 /** 041 * Split large file 042 * 043 * @param inputFile Input file 044 * @param outputDir Output directory 045 * @param splitSizeMB Split size (MB) 046 * @param dbVendor Database vendor 047 * @return List of split files 048 * @throws IOException IO exception 049 */ 050 public static List<File> splitFile(File inputFile, File outputDir, int splitSizeMB, EDbVendor dbVendor) throws IOException { 051 List<File> splitFiles = new ArrayList<>(); 052 if (!inputFile.exists()) { 053 throw new FileNotFoundException("Input file does not exist: " + inputFile.getAbsolutePath()); 054 } 055 056 if (!outputDir.exists() && !outputDir.mkdirs()) { 057 throw new IOException("Failed to create output directory: " + outputDir.getAbsolutePath()); 058 } 059 060 // Check if it's a JSON file 061 if (isJsonFile(inputFile)) { 062 return splitJsonFile(inputFile, outputDir, splitSizeMB); 063 } 064 065 // Process regular SQL file 066 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 067 068 try (RandomAccessFile raf = new RandomAccessFile(inputFile, "r")) { 069 long fileLength = raf.length(); 070 long currentPos = 0; 071 long chunkStartPos = 0; 072 int consecutiveFailures = 0; 073 int fileIndex = 1; 074 long globalStartLineNo = 1; 075 076 while (currentPos < fileLength) { 077 Long lastValidatedLineNo = getLastLineFromCurrentPos(raf, currentPos, fileLength, splitSizeBytes, dbVendor); 078 if (lastValidatedLineNo == null) break; 079 080 while (lastValidatedLineNo != null && lastValidatedLineNo == -1) { 081 consecutiveFailures++; 082 logger.warn("No validated statement found at position " + currentPos + " (consecutive failures: " + consecutiveFailures + ")"); 083 084 long incrementSize = Math.min(splitSizeBytes, fileLength - currentPos); 085 long previousPos = currentPos; 086 currentPos = Math.min(currentPos + incrementSize, fileLength); 087 088 if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { 089 logger.error("Reached maximum consecutive failures (" + MAX_CONSECUTIVE_FAILURES + "), skipping this segment and moving to next"); 090 long skipSize = splitSizeBytes * (consecutiveFailures + 1); 091 currentPos = Math.min(previousPos + skipSize, fileLength); 092 chunkStartPos = currentPos; 093 consecutiveFailures = 0; // Reset consecutive failure counter 094 logger.info("Skipped to position " + currentPos + ", continuing with next segment"); 095 break; 096 } 097 098 lastValidatedLineNo = getLastLineFromCurrentPos(raf, currentPos, fileLength, splitSizeBytes * (consecutiveFailures + 1), dbVendor); 099 } 100 101 if (lastValidatedLineNo == null) { 102 break; 103 } 104 105 if (lastValidatedLineNo == -1) { 106 continue; 107 } 108 109 consecutiveFailures = 0; 110 long endPos = findLineEndPosition(raf, currentPos, lastValidatedLineNo); 111 long globalEndLineNo = globalStartLineNo + lastValidatedLineNo - 1; 112 113 File splitFile = splitFileByPosition(outputDir, inputFile, chunkStartPos, endPos, fileIndex, globalStartLineNo, globalEndLineNo); 114 splitFiles.add(splitFile); 115 116 chunkStartPos = endPos; 117 currentPos = endPos; 118 globalStartLineNo = globalEndLineNo + 1; 119 fileIndex++; 120 121 if (currentPos >= fileLength) { 122 break; 123 } 124 } 125 } 126 127 return splitFiles; 128 } 129 130 /** 131 * Check if file is a JSON file 132 */ 133 private static boolean isJsonFile(File file) { 134 try (BufferedReader reader = new BufferedReader(new FileReader(file))) { 135 int c; 136 // Skip whitespace characters 137 while ((c = reader.read()) != -1) { 138 if (!Character.isWhitespace(c)) { 139 // Check if first non-whitespace character is '{' 140 return c == '{'; 141 } 142 } 143 } catch (IOException e) { 144 logger.error("Error reading file to check if it's JSON: " + file.getAbsolutePath(), e); 145 } 146 return false; 147 } 148 149 /** 150 * Get file extension 151 */ 152 private static String getFileExtension(String fileName) { 153 int lastDotIndex = fileName.lastIndexOf('.'); 154 if (lastDotIndex == -1) { 155 return ""; 156 } 157 return fileName.substring(lastDotIndex + 1); 158 } 159 160 /** 161 * Get file name without extension 162 */ 163 private static String getFileNameWithoutExtension(String fileName) { 164 int lastDotIndex = fileName.lastIndexOf('.'); 165 if (lastDotIndex == -1) { 166 return fileName; 167 } 168 return fileName.substring(0, lastDotIndex); 169 } 170 171 /** 172 * Move file 173 */ 174 private static void moveFile(File source, File target) throws IOException { 175 if (!source.exists()) { 176 throw new FileNotFoundException("Source file does not exist: " + source.getAbsolutePath()); 177 } 178 if (target.exists() && !target.delete()) { 179 throw new IOException("Failed to delete existing target file: " + target.getAbsolutePath()); 180 } 181 if (!source.renameTo(target)) { 182 // If renameTo fails, try copy and delete 183 copyFile(source, target); 184 if (!source.delete()) { 185 throw new IOException("Failed to delete source file after copy: " + source.getAbsolutePath()); 186 } 187 } 188 } 189 190 /** 191 * Copy file 192 */ 193 private static void copyFile(File source, File target) throws IOException { 194 try (FileInputStream fis = new FileInputStream(source); 195 FileOutputStream fos = new FileOutputStream(target)) { 196 byte[] buffer = new byte[BUFFER_SIZE]; 197 int bytesRead; 198 while ((bytesRead = fis.read(buffer)) != -1) { 199 fos.write(buffer, 0, bytesRead); 200 } 201 } 202 } 203 204 /** 205 * Split a JSON metadata file by routing to the format-specific splitter. 206 * 207 * <p>Routing logic: 208 * <ul> 209 * <li>{@code format: "sqlflow-sharded"} → splitSqlflowShardedJson() 210 * (after version-gating via isSupportedSqlflowSharded)</li> 211 * <li>{@code createdBy} contains "sqldep" or "grabit" → splitSqldepGrabitJson()</li> 212 * <li>{@code createdBy} contains "sqlflow" (and no format key) → splitSqlflowJson()</li> 213 * </ul> 214 * 215 * @param inputFile the JSON file to split 216 * @param outputDir the directory to write split files 217 * @param splitSizeMB target split size in megabytes 218 * @return list of generated split files 219 */ 220 private static List<File> splitJsonFile(File inputFile, File outputDir, int splitSizeMB) throws IOException { 221 List<File> splitFiles = new ArrayList<>(); 222 223 // Read file content 224 String jsonContent = readFileContent(inputFile); 225 Map<?, ?> content = (Map<?, ?>) JSON.parseObject(jsonContent); 226 String createdBy = (String) content.get("createdBy"); 227 String format = (String) content.get("format"); 228 229 if (createdBy == null) { 230 logger.warn("JSON file does not contain 'createdBy' field, treating as regular file"); 231 return splitFiles; 232 } 233 234 // Check for sqlflow-sharded format (new version) 235 if ("sqlflow-sharded".equals(format)) { 236 if (!gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader 237 .isSupportedSqlflowSharded(jsonContent)) { 238 // Unknown future formatVersion: do not split/rewrite a manifest we 239 // may mis-read into legacy files that strip the version. 240 logger.warn("Unsupported sqlflow-sharded formatVersion in " + inputFile 241 + "; not split."); 242 return splitFiles; 243 } 244 splitFiles.addAll(splitSqlflowShardedJson(inputFile, outputDir, splitSizeMB, content)); 245 } else if (createdBy.toLowerCase().contains("sqldep") || createdBy.toLowerCase().contains("grabit")) { 246 splitFiles.addAll(splitSqldepGrabitJson(inputFile, outputDir, splitSizeMB, content, createdBy)); 247 } else if (createdBy.toLowerCase().contains("sqlflow")) { 248 splitFiles.addAll(splitSqlflowJson(inputFile, outputDir, splitSizeMB, content)); 249 } else { 250 logger.warn("Unknown JSON file type with createdBy: " + createdBy); 251 } 252 253 return splitFiles; 254 } 255 256 private static String readFileContent(File inputFile) { 257 return SQLUtil.getFileContent(inputFile); 258 } 259 260 /** 261 * Write JSON file 262 */ 263 private static void writeJsonFile(File file, Object content) throws IOException { 264 try (FileWriter writer = new FileWriter(file)) { 265 JSON.toJSONString(content, writer); 266 } 267 } 268 269 /** 270 * Split a sqldep or grabit format JSON file. 271 * 272 * <p>These formats have {@code queries[]} at the root level. The splitter: 273 * <ul> 274 * <li>Creates a metadata file (everything except queries) as the first 275 * output file.</li> 276 * <li>Splits the queries into batches by cumulative sourceCode size, 277 * preserving the original {@code createdBy} so the re-reader 278 * recognizes the format via {@code MetadataReader.isGrabit()}.</li> 279 * </ul> 280 * 281 * @param inputFile the original JSON file 282 * @param outputDir the directory to write split files 283 * @param splitSizeMB target split size in megabytes 284 * @param content the parsed JSON content 285 * @param createdBy the original createdBy string 286 * @return list of generated split files 287 */ 288 private static List<File> splitSqldepGrabitJson(File inputFile, File outputDir, int splitSizeMB, Map<?, ?> content, String createdBy) throws IOException { 289 List<File> splitFiles = new ArrayList<>(); 290 List<Map<?, ?>> queries = (List<Map<?, ?>>) content.get("queries"); 291 if (queries == null || queries.isEmpty()) { 292 logger.warn("No queries found in JSON file"); 293 return splitFiles; 294 } 295 296 // Remove queries field, create metadata file 297 @SuppressWarnings("unchecked") 298 Map<String, Object> metadataContent = new LinkedHashMap<>((Map<String, Object>) content); 299 metadataContent.remove("queries"); 300 301 String extension = getFileExtension(inputFile.getName()); 302 File metadataFile = new File(outputDir, getFileNameWithoutExtension(inputFile.getName()) + "_" + 0 + "_" + 1 + (extension.isEmpty() ? "" : "." + extension)); 303 writeJsonFile(metadataFile, metadataContent); 304 splitFiles.add(metadataFile); 305 logger.info("Created metadata file: " + metadataFile.getAbsolutePath()); 306 307 // Split queries 308 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 309 int length = 0; 310 List<Map<?, ?>> temp = new ArrayList<>(); 311 long startIndex = 1; 312 long endIndex = 1; 313 314 for (Map<?, ?> item : queries) { 315 temp.add(item); 316 String sourceCode = (String) item.get("sourceCode"); 317 if (sourceCode != null) { 318 length += sourceCode.length(); 319 } 320 321 if (length >= splitSizeBytes) { 322 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 323 Map<String, Object> jsonObject = new LinkedHashMap<>(); 324 jsonObject.put("createdBy", createdBy); 325 jsonObject.put("dbvendor", content.get("dbvendor")); 326 jsonObject.put("databases", new ArrayList<>()); 327 jsonObject.put("queries", temp); 328 writeJsonFile(queryFile, jsonObject); 329 splitFiles.add(queryFile); 330 logger.info("Created query file: " + queryFile.getAbsolutePath()); 331 332 temp.clear(); 333 length = 0; 334 startIndex = endIndex; 335 } 336 endIndex++; 337 } 338 339 // Process remaining queries 340 if (!temp.isEmpty()) { 341 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 342 Map<String, Object> jsonObject = new LinkedHashMap<>(); 343 jsonObject.put("createdBy", createdBy); 344 jsonObject.put("dbvendor", content.get("dbvendor")); 345 jsonObject.put("databases", new ArrayList<>()); 346 jsonObject.put("queries", temp); 347 writeJsonFile(queryFile, jsonObject); 348 splitFiles.add(queryFile); 349 logger.info("Created query file: " + queryFile.getAbsolutePath()); 350 } 351 352 return splitFiles; 353 } 354 355 /** 356 * Split an old sqlflow format JSON file. 357 * 358 * <p>The old sqlflow format has {@code servers[].queries[]} structure with 359 * no {@code format} key. The splitter: 360 * <ul> 361 * <li>Extracts all queries from all servers, removing them from the 362 * metadata.</li> 363 * <li>Creates a metadata file with the servers (minus queries) as the 364 * first output file.</li> 365 * <li>Splits the queries into batches by cumulative sourceCode size, 366 * writing each batch as a grabit-format file ({@code createdBy: "grabit 367 * v1.7.0"}, {@code queries[]} at root level) so the re-reader 368 * recognizes it via {@code MetadataReader.isGrabit()}.</li> 369 * </ul> 370 * 371 * @param inputFile the original JSON file 372 * @param outputDir the directory to write split files 373 * @param splitSizeMB target split size in megabytes 374 * @param content the parsed JSON content 375 * @return list of generated split files 376 */ 377 private static List<File> splitSqlflowJson(File inputFile, File outputDir, int splitSizeMB, Map<?, ?> content) throws IOException { 378 List<File> splitFiles = new ArrayList<>(); 379 List<Map<?, ?>> queries = new ArrayList<>(); 380 381 // Extract all queries 382 List<Map<?, ?>> servers = (List<Map<?, ?>>) content.get("servers"); 383 if (servers != null) { 384 for (Map<?, ?> serverObject : servers) { 385 @SuppressWarnings("unchecked") 386 List<Map<?, ?>> serverQueries = (List<Map<?, ?>>) serverObject.get("queries"); 387 if (serverQueries != null) { 388 queries.addAll(serverQueries); 389 serverObject.remove("queries"); 390 } 391 } 392 } 393 394 if (queries.isEmpty()) { 395 logger.warn("No queries found in SQLFlow JSON file"); 396 return splitFiles; 397 } 398 399 // Create metadata file 400 String extension = getFileExtension(inputFile.getName()); 401 File metadataFile = new File(outputDir, getFileNameWithoutExtension(inputFile.getName()) + "_" + 0 + "_" + 1 + (extension.isEmpty() ? "" : "." + extension)); 402 writeJsonFile(metadataFile, content); 403 splitFiles.add(metadataFile); 404 logger.info("Created metadata file: " + metadataFile.getAbsolutePath()); 405 406 // Split queries 407 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 408 int length = 0; 409 List<Map<?, ?>> temp = new ArrayList<>(); 410 long startIndex = 1; 411 long endIndex = 1; 412 413 for (Map<?, ?> item : queries) { 414 temp.add(item); 415 String sourceCode = (String) item.get("sourceCode"); 416 if (sourceCode != null) { 417 length += sourceCode.length(); 418 } 419 420 if (length >= splitSizeBytes) { 421 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 422 Map<String, Object> jsonObject = new LinkedHashMap<>(); 423 jsonObject.put("createdBy", "grabit v1.7.0"); 424 jsonObject.put("queries", temp); 425 writeJsonFile(queryFile, jsonObject); 426 splitFiles.add(queryFile); 427 logger.info("Created query file: " + queryFile.getAbsolutePath()); 428 429 temp.clear(); 430 length = 0; 431 startIndex = endIndex; 432 } 433 endIndex++; 434 } 435 436 // Process remaining queries 437 if (!temp.isEmpty()) { 438 File queryFile = createQueryFile(outputDir, inputFile, startIndex, endIndex, extension); 439 Map<String, Object> jsonObject = new LinkedHashMap<>(); 440 jsonObject.put("createdBy", "grabit v1.7.0"); 441 jsonObject.put("queries", temp); 442 writeJsonFile(queryFile, jsonObject); 443 splitFiles.add(queryFile); 444 logger.info("Created query file: " + queryFile.getAbsolutePath()); 445 } 446 447 return splitFiles; 448 } 449 450 //autogenerate 451 /** 452 * Split a sqlflow-sharded manifest into multiple legacy sqlflow files suitable 453 * for parallel processing. 454 * 455 * <h3>Format conversion</h3> 456 * Input: sqlflow-sharded ({@code format: "sqlflow-sharded"}, separate 457 * catalog/*.catalog.json and source/*.source.jsonl files). 458 * Output: old sqlflow format (no {@code format} key, {@code createdBy} 459 * containing "sqlflow", catalog and source inlined into 460 * {@code servers[].databases[]} / {@code servers[].queries[]}). 461 * 462 * <p>The conversion is necessary because the re-reader identifies the old 463 * sqlflow format via {@code MetadataReader.isSqlflow()}, which requires: 464 * <ul> 465 * <li>No {@code format} key present</li> 466 * <li>{@code createdBy} contains "sqlflow"</li> 467 * </ul> 468 * 469 * <h3>Split strategy</h3> 470 * <ul> 471 * <li>Catalog: if a single database's catalog exceeds splitSizeMB, keep it 472 * as one file; otherwise merge consecutive databases until the batch 473 * reaches splitSizeMB.</li> 474 * <li>Source: if a single database's source is below splitSizeMB, merge with 475 * the next; if above, split by record count keeping individual records 476 * complete.</li> 477 * </ul> 478 * 479 * <h3>Limitations</h3> 480 * Only catalog-topology (servers[].databases[]) is supported. Schema-topology 481 * (servers[].schemas[]) exports are skipped — the caller falls back to 482 * whole-file processing. 483 * 484 * @param inputFile the original manifest file 485 * @param outputDir the directory to write split files 486 * @param splitSizeMB target split size in megabytes 487 * @param manifest the parsed manifest JSON 488 * @return list of generated split files 489 */ 490 private static List<File> splitSqlflowShardedJson(File inputFile, File outputDir, int splitSizeMB, Map<?, ?> manifest) throws IOException { 491 List<File> splitFiles = new ArrayList<>(); 492 493 File baseDir = inputFile.getParentFile(); 494 if (baseDir == null) { 495 logger.warn("Cannot determine base directory from manifest file"); 496 return splitFiles; 497 } 498 499 String createdBy = (String) manifest.get("createdBy"); 500 List<Map<?, ?>> servers = (List<Map<?, ?>>) manifest.get("servers"); 501 if (servers == null || servers.isEmpty()) { 502 logger.warn("No servers found in sqlflow-sharded manifest"); 503 return splitFiles; 504 } 505 506 String extension = getFileExtension(inputFile.getName()); 507 long splitSizeBytes = splitSizeMB * 1024 * 1024L; 508 509 // Collect all database info first 510 List<DatabaseCatalogInfo> allCatalogs = new ArrayList<>(); 511 List<DatabaseSourceInfo> allSources = new ArrayList<>(); 512 513 for (Map<?, ?> server : servers) { 514 String serverName = (String) server.get("name"); 515 String dbVendor = (String) server.get("dbVendor"); 516 517 // NOTE: schema-topology exports (servers[].schemas[]) are 518 // intentionally NOT split here. This splitter's output writers emit 519 // catalog-topology (databases[]-wrapped) shards; emitting flat 520 // schema shards through them yields a manifest legacy readers load 521 // as 0 tables. Rather than produce a broken split, we skip schema 522 // shards so the caller falls back to whole-file processing, which 523 // the readers now handle correctly (SqlflowSharded* schema-topology 524 // support). Splitting schema-topology manifests needs schema-aware 525 // output writers + a round-trip test — tracked as a follow-up. 526 List<Map<?, ?>> databases = (List<Map<?, ?>>) server.get("databases"); 527 if (databases == null || databases.isEmpty()) { 528 continue; 529 } 530 531 for (Map<?, ?> database : databases) { 532 String databaseName = (String) database.get("name"); 533 String shardId = (String) database.get("shardId"); 534 535 // Collect catalog info 536 Map<?, ?> catalogInfo = (Map<?, ?>) database.get("catalog"); 537 if (catalogInfo != null) { 538 String catalogPath = (String) catalogInfo.get("path"); 539 File catalogFile = resolvePath(baseDir, catalogPath); 540 if (catalogFile != null && catalogFile.exists()) { 541 allCatalogs.add(new DatabaseCatalogInfo(serverName, dbVendor, databaseName, shardId, catalogFile)); 542 } 543 } 544 545 // Collect source info 546 Map<?, ?> sourceInfo = (Map<?, ?>) database.get("source"); 547 String sourceCompression = (String) manifest.get("sourceCompression"); 548 if (sourceInfo != null) { 549 String sourcePath = (String) sourceInfo.get("path"); 550 File sourceFile = resolvePath(baseDir, sourcePath); 551 if (sourceFile != null && sourceFile.exists()) { 552 allSources.add(new DatabaseSourceInfo(serverName, dbVendor, databaseName, shardId, sourceFile, sourceCompression)); 553 } 554 } 555 } 556 } 557 558 // Process catalogs: merge if total < splitSizeMB, otherwise keep single 559 List<File> catalogFiles = processCatalogs(outputDir, inputFile, extension, allCatalogs, splitSizeBytes, createdBy); 560 splitFiles.addAll(catalogFiles); 561 562 // Process sources: merge if total < splitSizeMB, split by size if > splitSizeMB 563 List<File> sourceFiles = processSources(outputDir, inputFile, extension, allSources, splitSizeBytes, createdBy); 564 splitFiles.addAll(sourceFiles); 565 566 return splitFiles; 567 } 568 569 /** 570 * Process catalog shards: batch small catalogs together, keep large ones 571 * alone. 572 * 573 * <p>Strategy: 574 * <ul> 575 * <li>If a single catalog exceeds splitSizeBytes, write it as a standalone 576 * file via {@link #createCatalogSingleFile}.</li> 577 * <li>Otherwise, accumulate catalogs into a batch. Flush the batch when 578 * adding the next catalog would exceed maxBatchSize (2× splitSizeBytes).</li> 579 * <li>Any remaining batch is flushed at the end.</li> 580 * </ul> 581 * 582 * <p>All output files use the old sqlflow format (no {@code format} key, 583 * {@code servers[].databases[]}) so the re-reader recognizes them via 584 * {@code MetadataReader.isSqlflow()}. 585 * 586 * @param outputDir the directory to write output files 587 * @param inputFile the original manifest file (used for naming) 588 * @param extension the file extension 589 * @param catalogs the list of catalog shards to process 590 * @param splitSizeBytes target split size in bytes 591 * @param createdBy the createdBy string (must contain "sqlflow") 592 * @return list of generated catalog files 593 */ 594 private static List<File> processCatalogs(File outputDir, File inputFile, String extension, 595 List<DatabaseCatalogInfo> catalogs, long splitSizeBytes, 596 String createdBy) throws IOException { 597 List<File> result = new ArrayList<>(); 598 599 if (catalogs == null || catalogs.isEmpty()) { 600 return result; 601 } 602 603 int catalogIndex = 1; 604 List<DatabaseCatalogInfo> batch = new ArrayList<>(); 605 int currentSize = 0; 606 long maxBatchSize = splitSizeBytes * 2; 607 608 for (DatabaseCatalogInfo catalog : catalogs) { 609 int catalogSize = (int) catalog.file.length(); 610 611 // If single catalog > splitSizeBytes (5MB), it must be alone 612 if (catalogSize > splitSizeBytes) { 613 // Save current batch first 614 if (!batch.isEmpty()) { 615 File batchFile = createCatalogBatchFile(outputDir, inputFile, extension, batch, catalogIndex, createdBy); 616 if (batchFile != null) { 617 result.add(batchFile); 618 logger.info("Created catalog batch file: " + batchFile.getAbsolutePath()); 619 } 620 catalogIndex++; 621 batch = new ArrayList<>(); 622 currentSize = 0; 623 } 624 // Write this large catalog alone 625 File singleFile = createCatalogSingleFile(outputDir, inputFile, catalog, extension, catalogIndex, createdBy); 626 if (singleFile != null) { 627 result.add(singleFile); 628 logger.info("Created catalog file: " + singleFile.getAbsolutePath()); 629 } 630 catalogIndex++; 631 } else { 632 // Check if adding this catalog would exceed 2*splitSize (10MB) 633 if (currentSize + catalogSize > maxBatchSize && !batch.isEmpty()) { 634 // Save current batch 635 File batchFile = createCatalogBatchFile(outputDir, inputFile, extension, batch, catalogIndex, createdBy); 636 if (batchFile != null) { 637 result.add(batchFile); 638 logger.info("Created catalog batch file: " + batchFile.getAbsolutePath()); 639 } 640 catalogIndex++; 641 batch = new ArrayList<>(); 642 currentSize = 0; 643 } 644 batch.add(catalog); 645 currentSize += catalogSize; 646 } 647 } 648 649 // Save remaining batch 650 if (!batch.isEmpty()) { 651 File batchFile = createCatalogBatchFile(outputDir, inputFile, extension, batch, catalogIndex, createdBy); 652 if (batchFile != null) { 653 result.add(batchFile); 654 logger.info("Created catalog batch file: " + batchFile.getAbsolutePath()); 655 } 656 } 657 658 return result; 659 } 660 661 /** 662 * Process source shards: read all records from JSONL source files, split them 663 * into batches by cumulative sourceCode size. 664 * 665 * <p>Each source record is enriched with its database name and preserved 666 * fields ({@code schema}, {@code name}, {@code type}, {@code sourceCode}, 667 * {@code groupName}, {@code sourceUnavailable}, {@code sourceUnavailableReason}) 668 * so the downstream analyzer has the full per-query context. 669 * 670 * <p>Output files use the old sqlflow format ({@code servers[].queries[]}, 671 * no {@code format} key) with {@code createdBy} set to the caller-provided 672 * value. The re-reader must contain "sqlflow" to be recognized by 673 * {@code MetadataReader.isSqlflow()}. 674 * 675 * @param outputDir the directory to write output files 676 * @param inputFile the original manifest file (used for naming) 677 * @param extension the file extension 678 * @param sources the list of source shards to process 679 * @param splitSizeBytes target split size in bytes 680 * @param createdBy the createdBy string (must contain "sqlflow") 681 * @return list of generated source files 682 */ 683 private static List<File> processSources(File outputDir, File inputFile, String extension, 684 List<DatabaseSourceInfo> sources, long splitSizeBytes, 685 String createdBy) throws IOException { 686 List<File> result = new ArrayList<>(); 687 688 if (sources == null || sources.isEmpty()) { 689 return result; 690 } 691 692 int startIndex = 1; 693 List<SourceQueryEntry> currentBatch = new ArrayList<>(); 694 int currentSize = 0; 695 List<String> mergedFrom = new ArrayList<>(); 696 697 for (DatabaseSourceInfo source : sources) { 698 mergedFrom.add(source.shardId); 699 700 List<Map<?, ?>> records = readSourceRecords(source.file, source.compression); 701 if (records == null || records.isEmpty()) { 702 continue; 703 } 704 705 for (Map<?, ?> record : records) { 706 Map<String, Object> query = new LinkedHashMap<>(); 707 query.put("database", source.databaseName); 708 query.put("schema", record.get("schema")); 709 query.put("name", record.get("name")); 710 query.put("type", record.get("type")); 711 query.put("sourceCode", record.get("sourceCode")); 712 if (record.containsKey("groupName")) { 713 query.put("groupName", record.get("groupName")); 714 } 715 // Preserve the availability flag through the split, or the 716 // downstream analyzer cannot tell an unavailable (encrypted / 717 // permission-denied) record from a real one and would parse its 718 // placeholder text as live SQL. 719 if (record.containsKey("sourceUnavailable")) { 720 query.put("sourceUnavailable", record.get("sourceUnavailable")); 721 } 722 if (record.containsKey("sourceUnavailableReason")) { 723 query.put("sourceUnavailableReason", record.get("sourceUnavailableReason")); 724 } 725 726 Object sourceCode = record.get("sourceCode"); 727 int recordSize = sourceCode != null ? sourceCode.toString().length() : 0; 728 729 if (currentSize + recordSize > splitSizeBytes && !currentBatch.isEmpty()) { 730 int endIndex = startIndex + currentBatch.size() - 1; 731 File batchFile = createSourceQueryFileCommon(outputDir, inputFile, extension, 732 currentBatch, mergedFrom, startIndex, endIndex, createdBy); 733 if (batchFile != null) { 734 result.add(batchFile); 735 logger.info("Created source batch file: " + batchFile.getAbsolutePath()); 736 } 737 startIndex = endIndex + 1; 738 currentBatch = new ArrayList<>(); 739 currentSize = 0; 740 } 741 742 currentBatch.add(new SourceQueryEntry(source.serverName, source.dbVendor, query)); 743 currentSize += recordSize; 744 } 745 } 746 747 // Save remaining batch 748 if (!currentBatch.isEmpty()) { 749 int endIndex = startIndex + currentBatch.size() - 1; 750 File batchFile = createSourceQueryFileCommon(outputDir, inputFile, extension, 751 currentBatch, mergedFrom, startIndex, endIndex, createdBy); 752 if (batchFile != null) { 753 result.add(batchFile); 754 logger.info("Created source batch file: " + batchFile.getAbsolutePath()); 755 } 756 } 757 758 return result; 759 } 760 761 /** 762 * A single source query record with its server context (serverName, dbVendor). 763 * Used to group queries by server when writing split output files. 764 */ 765 private static class SourceQueryEntry { 766 String serverName; 767 String dbVendor; 768 Map<String, Object> query; 769 770 SourceQueryEntry(String serverName, String dbVendor, Map<String, Object> query) { 771 this.serverName = serverName; 772 this.dbVendor = dbVendor; 773 this.query = query; 774 } 775 } 776 777 /** 778 * Calculate total size of source records 779 */ 780 private static int calculateSourceSize(List<Map<?, ?>> records) { 781 if (records == null) return 0; 782 int size = 0; 783 for (Map<?, ?> record : records) { 784 Object sourceCode = record.get("sourceCode"); 785 if (sourceCode != null) { 786 size += sourceCode.toString().length(); 787 } 788 } 789 return size; 790 } 791 792 /** 793 * Create a catalog file for a batch of databases by converting sqlflow-sharded 794 * catalog shards into the old sqlflow format. 795 * 796 * <p>The output uses the old sqlflow format ({@code servers[].databases[]}, 797 * no {@code format} key) so that the re-reader recognizes it via 798 * {@link gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader#isSqlflow(String)}. 799 * The {@code createdBy} must contain "sqlflow" for the re-reader to recognize 800 * the format. 801 * 802 * @param outputDir the directory to write the output file 803 * @param inputFile the original manifest file (used for naming) 804 * @param extension the file extension to use 805 * @param batch the batch of catalog shards to merge 806 * @param batchIndex the batch index for file naming 807 * @param createdBy the createdBy string (must contain "sqlflow") 808 * @return the created catalog file, or null if the batch is empty 809 */ 810 private static File createCatalogBatchFile(File outputDir, File inputFile, String extension, 811 List<DatabaseCatalogInfo> batch, int batchIndex, 812 String createdBy) throws IOException { 813 if (batch == null || batch.isEmpty()) { 814 return null; 815 } 816 817 Map<String, Object> sqlflow = new LinkedHashMap<>(); 818 sqlflow.put("createdBy", createdBy != null ? createdBy : "sqlflow-ingester"); 819 820 // Add mergedFrom field to track original shardIds 821 List<String> mergedFrom = new ArrayList<>(); 822 823 List<Map<String, Object>> servers = new ArrayList<>(); 824 825 for (DatabaseCatalogInfo catalog : batch) { 826 mergedFrom.add(catalog.shardId); 827 828 Map<String, Object> catalogContent = readCatalogContent(catalog.file); 829 if (catalogContent == null) continue; 830 831 Map<String, Object> serverObj = new LinkedHashMap<>(); 832 serverObj.put("name", catalog.serverName); 833 serverObj.put("dbVendor", catalog.dbVendor); 834 835 Map<String, Object> dbObj = new LinkedHashMap<>(); 836 dbObj.put("name", catalog.databaseName); 837 838 copyCatalogFields(catalogContent, dbObj); 839 840 List<Map<String, Object>> databases = new ArrayList<>(); 841 databases.add(dbObj); 842 serverObj.put("databases", databases); 843 servers.add(serverObj); 844 } 845 846 sqlflow.put("servers", servers); 847 sqlflow.put("mergedFrom", mergedFrom); 848 849 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_catalog_" + batchIndex + 850 (extension.isEmpty() ? "" : "." + extension); 851 852 File outputFile = new File(outputDir, fileName); 853 writeJsonFile(outputFile, sqlflow); 854 return outputFile; 855 } 856 857 /** 858 * Create a catalog file for a single large database by converting its 859 * sqlflow-sharded catalog shard into the old sqlflow format. 860 * 861 * <p>Same format as {@link #createCatalogBatchFile} but for a single 862 * database whose catalog alone exceeds the split size threshold. The output 863 * uses the old sqlflow format ({@code servers[].databases[]}, no 864 * {@code format} key) so the re-reader recognizes it via 865 * {@code MetadataReader.isSqlflow()}. 866 * 867 * @param outputDir the directory to write the output file 868 * @param inputFile the original manifest file (used for naming) 869 * @param catalog the catalog shard to convert 870 * @param extension the file extension to use 871 * @param catalogIndex the catalog index for file naming 872 * @param createdBy the createdBy string (must contain "sqlflow") 873 * @return the created catalog file, or null on failure 874 */ 875 private static File createCatalogSingleFile(File outputDir, File inputFile, DatabaseCatalogInfo catalog, 876 String extension, int catalogIndex, String createdBy) throws IOException { 877 Map<String, Object> catalogContent = readCatalogContent(catalog.file); 878 if (catalogContent == null) { 879 return null; 880 } 881 882 Map<String, Object> sqlflow = new LinkedHashMap<>(); 883 sqlflow.put("createdBy", createdBy != null ? createdBy : "sqlflow-ingester"); 884 885 Map<String, Object> serverObj = new LinkedHashMap<>(); 886 serverObj.put("name", catalog.serverName); 887 serverObj.put("dbVendor", catalog.dbVendor); 888 889 Map<String, Object> dbObj = new LinkedHashMap<>(); 890 dbObj.put("name", catalog.databaseName); 891 copyCatalogFields(catalogContent, dbObj); 892 893 List<Map<String, Object>> databases = new ArrayList<>(); 894 databases.add(dbObj); 895 serverObj.put("databases", databases); 896 897 List<Map<String, Object>> servers = new ArrayList<>(); 898 servers.add(serverObj); 899 sqlflow.put("servers", servers); 900 901 // Add mergedFrom field to track original shardId 902 List<String> mergedFrom = new ArrayList<>(); 903 mergedFrom.add(catalog.shardId); 904 sqlflow.put("mergedFrom", mergedFrom); 905 906 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_catalog_" + catalogIndex + 907 (extension.isEmpty() ? "" : "." + extension); 908 909 File outputFile = new File(outputDir, fileName); 910 writeJsonFile(outputFile, sqlflow); 911 return outputFile; 912 } 913 914 /** 915 * Read catalog content from file 916 */ 917 private static Map<String, Object> readCatalogContent(File catalogFile) throws IOException { 918 String content = readFileContent(catalogFile); 919 @SuppressWarnings("unchecked") 920 Map<String, Object> catalog = (Map<String, Object>) JSON.parseObject(content); 921 return catalog; 922 } 923 924 /** 925 * Copy catalog metadata fields from the raw catalog shard into the database 926 * object of the output file. Preserves schemas, tables, views, procedures, 927 * functions, triggers, packages, and synonyms. 928 */ 929 private static void copyCatalogFields(Map<String, Object> catalog, Map<String, Object> dbObj) { 930 if (catalog.containsKey("schemas")) dbObj.put("schemas", catalog.get("schemas")); 931 if (catalog.containsKey("tables")) dbObj.put("tables", catalog.get("tables")); 932 if (catalog.containsKey("views")) dbObj.put("views", catalog.get("views")); 933 if (catalog.containsKey("procedures")) dbObj.put("procedures", catalog.get("procedures")); 934 if (catalog.containsKey("functions")) dbObj.put("functions", catalog.get("functions")); 935 if (catalog.containsKey("triggers")) dbObj.put("triggers", catalog.get("triggers")); 936 if (catalog.containsKey("packages")) dbObj.put("packages", catalog.get("packages")); 937 if (catalog.containsKey("synonyms")) dbObj.put("synonyms", catalog.get("synonyms")); 938 } 939 940 /** 941 * Create a source query file for a batch of records by writing them in the 942 * old sqlflow format. 943 * 944 * <p>Output format: {@code servers[].queries[]} with no {@code format} key. 945 * Queries are grouped by server (serverName + dbVendor). The 946 * {@code createdBy} is set from the caller-provided value and must contain 947 * "sqlflow" for the re-reader to recognize the format via 948 * {@code MetadataReader.isSqlflow()}. 949 * 950 * <p>File naming: {@code <inputName>_source_<startIndex>_<endIndex>.<ext>} 951 * 952 * @param outputDir the directory to write the output file 953 * @param inputFile the original manifest file (used for naming) 954 * @param extension the file extension 955 * @param entries the source query records to write 956 * @param mergedFrom the list of original shardIds 957 * @param startIndex the start record index (for file naming) 958 * @param endIndex the end record index (for file naming) 959 * @param createdBy the createdBy string (must contain "sqlflow") 960 * @return the created file 961 */ 962 private static File createSourceQueryFileCommon(File outputDir, File inputFile, String extension, 963 List<SourceQueryEntry> entries, List<String> mergedFrom, 964 int startIndex, int endIndex, 965 String createdBy) throws IOException { 966 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_source_" + startIndex + "_" + endIndex + 967 (extension.isEmpty() ? "" : "." + extension); 968 969 File queryFile = new File(outputDir, fileName); 970 971 Map<String, Object> sqlflow = new LinkedHashMap<>(); 972 sqlflow.put("createdBy", createdBy != null ? createdBy : "grabit v1.7.0"); 973 sqlflow.put("mergedFrom", mergedFrom); 974 975 // Group queries by server (preserve insertion order) 976 Map<String, Map<String, Object>> serverMap = new LinkedHashMap<>(); 977 for (SourceQueryEntry entry : entries) { 978 String key = (entry.serverName == null ? "" : entry.serverName) + "|" + (entry.dbVendor == null ? "" : entry.dbVendor); 979 Map<String, Object> serverObj = serverMap.get(key); 980 if (serverObj == null) { 981 serverObj = new LinkedHashMap<>(); 982 serverObj.put("name", entry.serverName); 983 if (entry.dbVendor != null) { 984 serverObj.put("dbVendor", entry.dbVendor); 985 } 986 serverObj.put("queries", new ArrayList<Map<String, Object>>()); 987 serverMap.put(key, serverObj); 988 } 989 @SuppressWarnings("unchecked") 990 List<Map<String, Object>> queries = (List<Map<String, Object>>) serverObj.get("queries"); 991 queries.add(entry.query); 992 } 993 994 List<Map<String, Object>> servers = new ArrayList<>(serverMap.values()); 995 sqlflow.put("servers", servers); 996 997 writeJsonFile(queryFile, sqlflow); 998 return queryFile; 999 } 1000 1001 /** 1002 * Resolve path relative to base directory 1003 */ 1004 private static File resolvePath(File baseDir, String path) { 1005 if (path == null || path.isEmpty()) { 1006 return null; 1007 } 1008 1009 File file = new File(path); 1010 if (file.isAbsolute()) { 1011 return file; 1012 } 1013 1014 return new File(baseDir, path); 1015 } 1016 1017 /** 1018 * Read source records from a JSONL file. 1019 * 1020 * <p>Supports two compression modes: 1021 * <ul> 1022 * <li>{@code "block"} — gzip-compressed JSONL, read via GZIPInputStream.</li> 1023 * <li>Otherwise — plain text JSONL, split by newline.</li> 1024 * </ul> 1025 * 1026 * @param sourceFile the JSONL source file 1027 * @param compression the compression mode ("block" or null) 1028 * @return list of parsed records, or null if the file cannot be read 1029 */ 1030 private static List<Map<?, ?>> readSourceRecords(File sourceFile, String compression) throws IOException { 1031 if (sourceFile == null || !sourceFile.exists()) { 1032 return null; 1033 } 1034 1035 List<Map<?, ?>> records = new ArrayList<>(); 1036 1037 if ("block".equals(compression)) { 1038 // Read gzip compressed file 1039 try (java.util.zip.GZIPInputStream gzis = new java.util.zip.GZIPInputStream( 1040 new java.io.FileInputStream(sourceFile))) { 1041 java.io.BufferedReader reader = new java.io.BufferedReader( 1042 new java.io.InputStreamReader(gzis, "UTF-8")); 1043 String line; 1044 while ((line = reader.readLine()) != null) { 1045 if (!line.trim().isEmpty()) { 1046 Map<?, ?> record = (Map<?, ?>) JSON.parseObject(line); 1047 records.add(record); 1048 } 1049 } 1050 } 1051 } else { 1052 // Read plain JSONL file 1053 String content = readFileContent(sourceFile); 1054 String[] lines = content.split("\n"); 1055 for (String line : lines) { 1056 if (!line.trim().isEmpty()) { 1057 Map<?, ?> record = (Map<?, ?>) JSON.parseObject(line); 1058 records.add(record); 1059 } 1060 } 1061 } 1062 1063 return records; 1064 } 1065 1066 /** 1067 * Holds metadata for a single catalog shard: server, database, and the 1068 * catalog file to read. 1069 */ 1070 private static class DatabaseCatalogInfo { 1071 String serverName; 1072 String dbVendor; 1073 String databaseName; 1074 String shardId; 1075 File file; 1076 1077 DatabaseCatalogInfo(String serverName, String dbVendor, String databaseName, String shardId, File file) { 1078 this.serverName = serverName; 1079 this.dbVendor = dbVendor; 1080 this.databaseName = databaseName; 1081 this.shardId = shardId; 1082 this.file = file; 1083 } 1084 } 1085 1086 /** 1087 * Holds metadata for a single source shard: server, database, source file, 1088 * and compression mode. 1089 */ 1090 private static class DatabaseSourceInfo { 1091 String serverName; 1092 String dbVendor; 1093 String databaseName; 1094 String shardId; 1095 File file; 1096 String compression; 1097 1098 DatabaseSourceInfo(String serverName, String dbVendor, String databaseName, String shardId, File file, String compression) { 1099 this.serverName = serverName; 1100 this.dbVendor = dbVendor; 1101 this.databaseName = databaseName; 1102 this.shardId = shardId; 1103 this.file = file; 1104 this.compression = compression; 1105 } 1106 } 1107 1108 /** 1109 * Create query file 1110 */ 1111 private static File createQueryFile(File outputDir, File inputFile, long startIndex, long endIndex, String extension) { 1112 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_" + startIndex + "_" + endIndex + (extension.isEmpty() ? "" : "." + extension); 1113 return new File(outputDir, fileName); 1114 } 1115 1116 private static Long getLastLineFromCurrentPos(RandomAccessFile raf, long currentPos, long fileLength, long splitSizeBytes, EDbVendor vendor) throws IOException { 1117 raf.seek(currentPos); 1118 1119 long remainingBytes = fileLength - currentPos; 1120 long readSize = Math.min(splitSizeBytes, remainingBytes); 1121 1122 byte[] buffer = new byte[(int) readSize]; 1123 int bytesRead = raf.read(buffer); 1124 if (bytesRead <= 0) { 1125 return null; 1126 } 1127 1128 String partialContent = new String(buffer, 0, bytesRead, "UTF-8"); 1129 if (partialContent.isEmpty()) { 1130 return null; 1131 } 1132 1133 TGSqlParser parser = new TGSqlParser(vendor); 1134 parser.sqltext = partialContent; 1135 parser.getrawsqlstatements(); 1136 1137 long lastValidatedLineNo = parser.getLastLineNoOfLastStatementBeenValidated(); 1138 return lastValidatedLineNo; 1139 } 1140 1141 /** 1142 * Find end position of Nth line from specified byte position 1143 * Supports multiple newline formats: \n (Unix/Linux), \r\n (Windows), \r (Old Mac) 1144 * Uses buffer for batch reading to improve performance 1145 */ 1146 private static long findLineEndPosition(RandomAccessFile raf, long startPos, long lineCount) throws IOException { 1147 raf.seek(startPos); 1148 long newlineCount = 0; 1149 long fileLength = raf.length(); 1150 1151 byte[] buffer = new byte[BUFFER_SIZE]; 1152 long currentPos = startPos; 1153 1154 while (currentPos < fileLength) { 1155 int toRead = (int) Math.min(buffer.length, fileLength - currentPos); 1156 int bytesRead = raf.read(buffer, 0, toRead); 1157 if (bytesRead <= 0) { 1158 break; 1159 } 1160 1161 for (int i = 0; i < bytesRead; i++) { 1162 byte b = buffer[i]; 1163 1164 if (b == '\n') { 1165 newlineCount++; 1166 if (newlineCount == lineCount) { 1167 return currentPos + i + 1; 1168 } 1169 } else if (b == '\r') { 1170 if (i + 1 < bytesRead) { 1171 if (buffer[i + 1] == '\n') { 1172 newlineCount++; 1173 if (newlineCount == lineCount) { 1174 return currentPos + i + 2; 1175 } 1176 i++; 1177 } else { 1178 newlineCount++; 1179 if (newlineCount == lineCount) { 1180 return currentPos + i + 1; 1181 } 1182 } 1183 } else { 1184 long savedPos = raf.getFilePointer(); 1185 int nextByte = raf.read(); 1186 1187 if (nextByte == '\n') { 1188 newlineCount++; 1189 if (newlineCount == lineCount) { 1190 return currentPos + i + 2; 1191 } 1192 currentPos++; 1193 } else { 1194 newlineCount++; 1195 if (newlineCount == lineCount) { 1196 return currentPos + i + 1; 1197 } 1198 if (nextByte != -1) { 1199 raf.seek(savedPos); 1200 } 1201 } 1202 } 1203 } 1204 } 1205 1206 currentPos += bytesRead; 1207 } 1208 1209 return fileLength; 1210 } 1211 1212 /** 1213 * Split file by byte position 1214 */ 1215 private static File splitFileByPosition(File outputDir, File inputFile, long startPos, long endPos, int fileIndex, long startLineNo, long endLineNo) throws IOException { 1216 String fileName = getFileNameWithoutExtension(inputFile.getName()) + "_" + fileIndex + "_" + startLineNo + "_" + endLineNo + "." + getFileExtension(inputFile.getName()); 1217 File outputFile = new File(outputDir, fileName); 1218 1219 try (RandomAccessFile rafRead = new RandomAccessFile(inputFile, "r"); 1220 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), java.nio.charset.StandardCharsets.UTF_8))) { 1221 1222 rafRead.seek(startPos); 1223 long bytesToRead = endPos - startPos; 1224 long bytesRemaining = bytesToRead; 1225 1226 byte[] buffer = new byte[BUFFER_SIZE]; 1227 1228 while (bytesRemaining > 0) { 1229 int toRead = (int) Math.min(buffer.length, bytesRemaining); 1230 int bytesRead = rafRead.read(buffer, 0, toRead); 1231 if (bytesRead <= 0) { 1232 break; 1233 } 1234 1235 String chunk = new String(buffer, 0, bytesRead, java.nio.charset.StandardCharsets.UTF_8); 1236 writer.write(chunk); 1237 bytesRemaining -= bytesRead; 1238 } 1239 1240 logger.info("split file " + inputFile.getName() + " (index: " + fileIndex + ", lines: " + startLineNo + "-" + endLineNo + ") from byte " + startPos + " to " + endPos + " to " + outputFile.getName()); 1241 1242 return outputFile; 1243 } 1244 } 1245}