| 1 | package com.renomad.minum.database; | |
| 2 | ||
| 3 | import com.renomad.minum.logging.ILogger; | |
| 4 | import com.renomad.minum.state.Context; | |
| 5 | import com.renomad.minum.utils.IFileUtils; | |
| 6 | ||
| 7 | import java.io.BufferedReader; | |
| 8 | import java.io.FileReader; | |
| 9 | import java.io.IOException; | |
| 10 | import java.nio.file.Path; | |
| 11 | import java.text.ParseException; | |
| 12 | import java.util.ArrayList; | |
| 13 | import java.util.Comparator; | |
| 14 | import java.util.List; | |
| 15 | import java.util.stream.Stream; | |
| 16 | ||
| 17 | import static com.renomad.minum.utils.Invariants.mustBeFalse; | |
| 18 | import static java.nio.charset.StandardCharsets.US_ASCII; | |
| 19 | import static java.nio.file.StandardOpenOption.*; | |
| 20 | ||
| 21 | /** | |
| 22 | * This class exists to handle converting from one file/folder | |
| 23 | * style of database to another. | |
| 24 | */ | |
| 25 | final class DbFileConverter { | |
| 26 | ||
| 27 | private final Path dbDirectory; | |
| 28 | private final ILogger logger; | |
| 29 | private final DatabaseAppender databaseAppender; | |
| 30 | private final DatabaseConsolidator databaseConsolidator; | |
| 31 | private final IFileUtils fileUtils; | |
| 32 | ||
| 33 | /** | |
| 34 | * Construct a converter instance | |
| 35 | * @param context this is used for its constants, logger, and so on. | |
| 36 | * @param dbDirectory this is the specific directory for this database, | |
| 37 | * for example, the full path to the "users" database directory. | |
| 38 | */ | |
| 39 | DbFileConverter(Context context, Path dbDirectory, IFileUtils fileUtils) { | |
| 40 | this.dbDirectory = dbDirectory; | |
| 41 | this.logger = context.getLogger(); | |
| 42 | this.fileUtils = fileUtils; | |
| 43 | try { | |
| 44 | this.databaseAppender = new DatabaseAppender(dbDirectory, context, fileUtils); | |
| 45 | this.databaseConsolidator = new DatabaseConsolidator(dbDirectory, context, fileUtils); | |
| 46 | } catch (IOException ex) { | |
| 47 | throw new DbException("Error in DbFileConverter constructor", ex); | |
| 48 | } | |
| 49 | ||
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * convert a directory of database files from the old Db classic form to the DbEngine2 form. | |
| 54 | * The old form was one file per data item, the new form has append-only | |
| 55 | * data logs and consolidated files. | |
| 56 | */ | |
| 57 | void convertClassicFolderStructureToDbEngine2Form() throws IOException { | |
| 58 |
1
1. convertClassicFolderStructureToDbEngine2Form : removed call to com/renomad/minum/database/DbFileConverter::displayWarningConvertingClassicToDbEngine2 → TIMED_OUT |
displayWarningConvertingClassicToDbEngine2(); |
| 59 | try (var fileReader = new FileReader(dbDirectory.resolve("index.ddps").toFile(), US_ASCII); | |
| 60 | BufferedReader br = new BufferedReader(fileReader)) { | |
| 61 | String s = br.readLine(); | |
| 62 |
1
1. convertClassicFolderStructureToDbEngine2Form : negated conditional → KILLED |
if (s == null) |
| 63 | throw new DbException("index file for " + dbDirectory + " returned null when reading a line from it"); | |
| 64 | mustBeFalse(s.isBlank(), "Unless something is terribly broken, we expect a numeric value here"); | |
| 65 | } | |
| 66 | ||
| 67 |
1
1. convertClassicFolderStructureToDbEngine2Form : removed call to com/renomad/minum/database/DbFileConverter::walkFilesAndConvertDbToDbEngine2 → TIMED_OUT |
walkFilesAndConvertDbToDbEngine2(this.dbDirectory, this.logger, fileUtils); |
| 68 | } | |
| 69 | ||
| 70 | ||
| 71 | ||
| 72 | private void displayWarningConvertingClassicToDbEngine2() { | |
| 73 | logger.logDebug(() -> "*****************************************************************"); | |
| 74 | logger.logDebug(() -> "*****************************************************************"); | |
| 75 | logger.logDebug(() -> "........"); | |
| 76 | logger.logDebug(() -> "About to convert database files from Db Classic to Db Engine 2"); | |
| 77 | logger.logDebug(() -> "........"); | |
| 78 | logger.logDebug(() -> "*****************************************************************"); | |
| 79 | logger.logDebug(() -> "*****************************************************************"); | |
| 80 | } | |
| 81 | ||
| 82 | private void displayWarningConvertingDbEngine2ToClassic() { | |
| 83 | logger.logDebug(() -> "*****************************************************************"); | |
| 84 | logger.logDebug(() -> "*****************************************************************"); | |
| 85 | logger.logDebug(() -> "........"); | |
| 86 | logger.logDebug(() -> "About to convert database files from Db Engine2 to Db Classic"); | |
| 87 | logger.logDebug(() -> "........"); | |
| 88 | logger.logDebug(() -> "*****************************************************************"); | |
| 89 | logger.logDebug(() -> "*****************************************************************"); | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| 93 | * walk through all the files in this directory, collecting | |
| 94 | * all regular files (non-subdirectories) except for index.ddps | |
| 95 | */ | |
| 96 | static void walkFilesAndConvertDbToDbEngine2(Path dbDirectory, ILogger logger, IFileUtils fileUtils) throws IOException { | |
| 97 | List<Path> listOfFiles = getListOfFiles(dbDirectory, fileUtils); | |
| 98 | // convert each file to the new database schema by appending it | |
| 99 | // to the append log, and then delete it | |
| 100 |
2
1. walkFilesAndConvertDbToDbEngine2 : changed conditional boundary → TIMED_OUT 2. walkFilesAndConvertDbToDbEngine2 : negated conditional → KILLED |
for (int i = 0; i < listOfFiles.size(); i++) { |
| 101 |
1
1. walkFilesAndConvertDbToDbEngine2 : Replaced integer division with multiplication → TIMED_OUT |
int percentCompletion = listOfFiles.size() / 100; |
| 102 |
1
1. walkFilesAndConvertDbToDbEngine2 : negated conditional → TIMED_OUT |
if (i == percentCompletion) { |
| 103 | logger.logDebug(() -> "File converting is %d percent complete".formatted(percentCompletion)); | |
| 104 | } | |
| 105 | Path p = extractDataAndAppend(dbDirectory, logger, listOfFiles.get(i), fileUtils); | |
| 106 |
1
1. walkFilesAndConvertDbToDbEngine2 : removed call to com/renomad/minum/utils/IFileUtils::delete → TIMED_OUT |
fileUtils.delete(p); |
| 107 | } | |
| 108 | ||
| 109 | // at this point, after all the ordinary files have been removed, kill the index file | |
| 110 |
1
1. walkFilesAndConvertDbToDbEngine2 : removed call to com/renomad/minum/utils/IFileUtils::delete → KILLED |
fileUtils.delete(dbDirectory.resolve("index.ddps")); |
| 111 | } | |
| 112 | ||
| 113 | /** | |
| 114 | * This method digs into the Db Classic file, checks everything is kosher, and | |
| 115 | * if so, appends it to the append-only log file which is part of DbEngine2 | |
| 116 | */ | |
| 117 | static Path extractDataAndAppend(Path dbDirectory, ILogger logger, Path fileToAnalyze, IFileUtils fileUtils) throws IOException { | |
| 118 | String fileContents = checkFileDetailsAreValid(fileToAnalyze, logger, fileUtils); | |
| 119 |
1
1. extractDataAndAppend : negated conditional → KILLED |
if (!fileContents.isBlank()) { |
| 120 |
1
1. extractDataAndAppend : removed call to com/renomad/minum/utils/IFileUtils::writeString → KILLED |
fileUtils.writeString( |
| 121 | dbDirectory.resolve("currentAppendLog"), | |
| 122 | "UPDATE %s\n".formatted(fileContents), APPEND, CREATE); | |
| 123 | } | |
| 124 |
1
1. extractDataAndAppend : replaced return value with null for com/renomad/minum/database/DbFileConverter::extractDataAndAppend → KILLED |
return fileToAnalyze; |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * Get the files that make up the file schema of Db Classic | |
| 129 | */ | |
| 130 | private static List<Path> getListOfFiles(Path dbDirectory, IFileUtils fileUtils) throws IOException { | |
| 131 | List<Path> listOfFiles; | |
| 132 | try (Stream<Path> fileStream = fileUtils.list(dbDirectory)) { | |
| 133 | listOfFiles = fileStream.filter(path -> | |
| 134 |
2
1. lambda$getListOfFiles$15 : replaced boolean return with true for com/renomad/minum/database/DbFileConverter::lambda$getListOfFiles$15 → TIMED_OUT 2. lambda$getListOfFiles$15 : negated conditional → KILLED |
fileUtils.isRegularFile(path) && |
| 135 |
1
1. lambda$getListOfFiles$15 : negated conditional → TIMED_OUT |
path.getFileName().toString().endsWith(".ddps") && |
| 136 |
1
1. lambda$getListOfFiles$15 : negated conditional → KILLED |
!path.getFileName().toString().startsWith("index")) |
| 137 | .toList(); | |
| 138 | } | |
| 139 |
1
1. getListOfFiles : replaced return value with Collections.emptyList for com/renomad/minum/database/DbFileConverter::getListOfFiles → TIMED_OUT |
return listOfFiles; |
| 140 | } | |
| 141 | ||
| 142 | /** | |
| 143 | * This code inspects that the data in the file is valid | |
| 144 | * and returns the data. This method is called as part of | |
| 145 | * convert Db Classic to DbEngine2 | |
| 146 | */ | |
| 147 | static String checkFileDetailsAreValid(Path p, ILogger logger, IFileUtils fileUtils) throws IOException { | |
| 148 |
1
1. checkFileDetailsAreValid : negated conditional → KILLED |
if (!fileUtils.isRegularFile(p)) { |
| 149 | throw new DbException("At checkFileDetailsAreValid, path " + p + " is not a regular file"); | |
| 150 | } | |
| 151 | String fileName = p.getFileName().toString(); | |
| 152 | int startOfSuffixIndex = fileName.indexOf('.'); | |
| 153 | String fileContents = fileUtils.readString(p); | |
| 154 |
1
1. checkFileDetailsAreValid : negated conditional → KILLED |
if (fileContents.isBlank()) { |
| 155 | logger.logDebug( () -> fileName + " file exists but empty, skipping"); | |
| 156 | return ""; | |
| 157 | } else { | |
| 158 |
1
1. checkFileDetailsAreValid : replaced return value with "" for com/renomad/minum/database/DbFileConverter::checkFileDetailsAreValid → KILLED |
return deserializeDataFromDbFile(fileName, startOfSuffixIndex, fileContents); |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * While converting Db Classic files to DbEngine2, each data file will be inspected. | |
| 164 | * Here, we are looking at the contents of the files. | |
| 165 | */ | |
| 166 | static String deserializeDataFromDbFile(String filename, int startOfSuffixIndex, String fileContents) { | |
| 167 | int fileNameIdentifier = Integer.parseInt(filename.substring(0, startOfSuffixIndex)); | |
| 168 | int indexOfFirstPipe = fileContents.indexOf('|'); | |
| 169 | String indexString = fileContents.substring(0, indexOfFirstPipe); | |
| 170 | long index = Long.parseLong(indexString); | |
| 171 |
1
1. deserializeDataFromDbFile : negated conditional → KILLED |
if (index != fileNameIdentifier) { |
| 172 | throw new DbException( "The filename (%s) must correspond to the index in its contents (%d)" | |
| 173 | .formatted(filename, index)); | |
| 174 | } | |
| 175 |
1
1. deserializeDataFromDbFile : replaced return value with "" for com/renomad/minum/database/DbFileConverter::deserializeDataFromDbFile → KILLED |
return fileContents; |
| 176 | } | |
| 177 | ||
| 178 | /** | |
| 179 | * Convert the folder/file structure. From DbEngine2 format to Db classic. | |
| 180 | */ | |
| 181 | void convertFolderStructureToDbClassic() throws IOException, ParseException { | |
| 182 |
1
1. convertFolderStructureToDbClassic : removed call to com/renomad/minum/database/DbFileConverter::displayWarningConvertingDbEngine2ToClassic → TIMED_OUT |
displayWarningConvertingDbEngine2ToClassic(); |
| 183 | ||
| 184 | // if there are any remnant items in the current append-only file, move them | |
| 185 | // to a new file | |
| 186 | databaseAppender.saveOffCurrentDataToReadyFolder(); | |
| 187 | ||
| 188 | // consolidate whatever files still exist in the append logs | |
| 189 |
1
1. convertFolderStructureToDbClassic : removed call to com/renomad/minum/database/DatabaseConsolidator::consolidate → KILLED |
databaseConsolidator.consolidate(); |
| 190 | ||
| 191 | // at this point, all the data is consolidated, in order, so we can step | |
| 192 | // through the data, creating new files, and finish up with an index.ddps | |
| 193 | // set to the proper value (i.e. one greater than the max index in the database) | |
| 194 |
1
1. convertFolderStructureToDbClassic : removed call to com/renomad/minum/database/DbFileConverter::walkFilesAndConvertDbEngine2ToDbClassic → TIMED_OUT |
walkFilesAndConvertDbEngine2ToDbClassic(this.dbDirectory, this.logger, fileUtils); |
| 195 | } | |
| 196 | ||
| 197 | /** | |
| 198 | * This is a pretty intricate method. It basically steps through the consolidated | |
| 199 | * data files, writing each line to a new file. It has to handle this through | |
| 200 | * multiple files and deleting files when finished, and closing file handles | |
| 201 | * appropriately when done with a file, and doing it in the proper order. | |
| 202 | */ | |
| 203 | static void walkFilesAndConvertDbEngine2ToDbClassic(Path dbDirectory, ILogger logger, IFileUtils fileUtils) throws IOException { | |
| 204 | // get the list of consolidated data files | |
| 205 | List<Path> listOfFiles; | |
| 206 | try (Stream<Path> fileStream = fileUtils.list(dbDirectory.resolve("consolidated_data"))) { | |
| 207 |
2
1. lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 : replaced boolean return with false for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 → TIMED_OUT 2. lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 : replaced boolean return with true for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$17 → KILLED |
listOfFiles = new ArrayList<>(fileStream.filter(x -> DbFileConverter.isDataFile(x, fileUtils)).toList()); |
| 208 | } | |
| 209 | ||
| 210 | // sort the data in ascending order. Files like "1_to_10" will come before "11_to_20" | |
| 211 | listOfFiles.sort(Comparator.comparing(x -> { | |
| 212 | String filename = x.getFileName().toString(); | |
| 213 | int indexOfFirstUnderscore = filename.indexOf('_'); | |
| 214 |
1
1. lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 : negated conditional → TIMED_OUT |
if (indexOfFirstUnderscore == -1) { |
| 215 | throw new DbException("Error: Failed to find first underscore in filename: " + filename); | |
| 216 | } | |
| 217 | String firstIndexNumberOfFile = filename.substring(0, indexOfFirstUnderscore); | |
| 218 | try { | |
| 219 |
1
1. lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 : replaced Long return value with 0L for com/renomad/minum/database/DbFileConverter::lambda$walkFilesAndConvertDbEngine2ToDbClassic$18 → KILLED |
return Long.parseLong(firstIndexNumberOfFile); |
| 220 | } catch (NumberFormatException ex) { | |
| 221 | throw new DbException("Failed to convert first part of filename to a number: " + filename); | |
| 222 | } | |
| 223 | })); | |
| 224 | ||
| 225 | // initialize a variable to record the current maximum index value | |
| 226 | long currentMaxIndexValue = -1; | |
| 227 | ||
| 228 | // initialize a variable to record the count of data items converted to Db Classic | |
| 229 | long countConvertedFiles = 0; | |
| 230 | ||
| 231 | // convert each line of each file to its own file, per the needs of Db Classic | |
| 232 | for (Path filePath : listOfFiles) { | |
| 233 | try (BufferedReader reader = fileUtils.newBufferedReader(filePath.toFile().toPath(), US_ASCII)) { | |
| 234 | String line; | |
| 235 | while ((line = reader.readLine()) != null) { | |
| 236 | int i = line.indexOf('|'); | |
| 237 | if (i == -1) { | |
| 238 | throw new DbException(("Unable to convert a line - check for " + | |
| 239 | "corruption. File: %s Data: %s").formatted(filePath, line)); | |
| 240 | } | |
| 241 | String indexNumberString = line.substring(0, i); | |
| 242 | long indexNumber; | |
| 243 | try { | |
| 244 | indexNumber = Long.parseLong(indexNumberString); | |
| 245 | currentMaxIndexValue = Math.max(indexNumber, currentMaxIndexValue); | |
| 246 | } catch (NumberFormatException ex) { | |
| 247 | throw new DbException(("Unable to convert a line - check for " + | |
| 248 | "corruption. File: %s Data: %s").formatted(filePath, line)); | |
| 249 | } | |
| 250 | String dbFilename = indexNumber + ".ddps"; | |
| 251 | Path dbFullPath = dbDirectory.resolve(dbFilename); | |
| 252 | fileUtils.writeString(dbFullPath, line, CREATE, WRITE); | |
| 253 | countConvertedFiles += 1; | |
| 254 | logAlongConversion(logger, countConvertedFiles, 1000); | |
| 255 | } | |
| 256 | // now we've converted everything from this file, delete it and its checksum (if available) | |
| 257 | fileUtils.delete(filePath); | |
| 258 | Path checksumPath = filePath.resolveSibling(filePath.getFileName() + ".checksum"); | |
| 259 | fileUtils.deleteIfExists(checksumPath); | |
| 260 | } | |
| 261 | } | |
| 262 | ||
| 263 |
1
1. walkFilesAndConvertDbEngine2ToDbClassic : removed call to com/renomad/minum/database/DbFileConverter::deleteEmptyDbEngine2Directories → KILLED |
deleteEmptyDbEngine2Directories(dbDirectory, fileUtils); |
| 264 |
2
1. walkFilesAndConvertDbEngine2ToDbClassic : Replaced long addition with subtraction → TIMED_OUT 2. walkFilesAndConvertDbEngine2ToDbClassic : removed call to com/renomad/minum/utils/IFileUtils::writeString → KILLED |
fileUtils.writeString(dbDirectory.resolve("index.ddps"), String.valueOf(currentMaxIndexValue + 1), CREATE_NEW); |
| 265 | } | |
| 266 | ||
| 267 | /** | |
| 268 | * A predicate used to filter for just regular database files, not checksum files | |
| 269 | */ | |
| 270 | static boolean isDataFile(Path path, IFileUtils fileUtils) { | |
| 271 |
3
1. isDataFile : negated conditional → TIMED_OUT 2. isDataFile : replaced boolean return with true for com/renomad/minum/database/DbFileConverter::isDataFile → TIMED_OUT 3. isDataFile : negated conditional → KILLED |
return fileUtils.isRegularFile(path) && !path.toString().contains("checksum"); |
| 272 | } | |
| 273 | ||
| 274 | /** | |
| 275 | * This method is called after conversion to Db classic, at which point these directories | |
| 276 | * will be empty. | |
| 277 | */ | |
| 278 | static void deleteEmptyDbEngine2Directories(Path dbDirectory, IFileUtils fileUtils) throws IOException { | |
| 279 | fileUtils.deleteIfExists(dbDirectory.resolve("consolidated_data")); | |
| 280 | fileUtils.deleteIfExists(dbDirectory.resolve("currentAppendLog")); | |
| 281 | fileUtils.deleteIfExists(dbDirectory.resolve("append_logs")); | |
| 282 | } | |
| 283 | ||
| 284 | /** | |
| 285 | * A helper to choose when to output logging statements during the conversion | |
| 286 | * of files from one database file format to another. | |
| 287 | * @param countConvertedFilesModulo modulo this number at which a log statement will be output. | |
| 288 | */ | |
| 289 | static void logAlongConversion(ILogger logger, long countConvertedFiles, int countConvertedFilesModulo) { | |
| 290 |
2
1. logAlongConversion : Replaced long modulus with multiplication → KILLED 2. logAlongConversion : negated conditional → KILLED |
if (countConvertedFiles % countConvertedFilesModulo == 0) { |
| 291 | logger.logDebug(() -> "DbFileConverter has converted %d files to Db Classic form" | |
| 292 | .formatted(countConvertedFiles)); | |
| 293 | } | |
| 294 | } | |
| 295 | } | |
Mutations | ||
| 58 |
1.1 |
|
| 62 |
1.1 |
|
| 67 |
1.1 |
|
| 100 |
1.1 2.2 |
|
| 101 |
1.1 |
|
| 102 |
1.1 |
|
| 106 |
1.1 |
|
| 110 |
1.1 |
|
| 119 |
1.1 |
|
| 120 |
1.1 |
|
| 124 |
1.1 |
|
| 134 |
1.1 2.2 |
|
| 135 |
1.1 |
|
| 136 |
1.1 |
|
| 139 |
1.1 |
|
| 148 |
1.1 |
|
| 154 |
1.1 |
|
| 158 |
1.1 |
|
| 171 |
1.1 |
|
| 175 |
1.1 |
|
| 182 |
1.1 |
|
| 189 |
1.1 |
|
| 194 |
1.1 |
|
| 207 |
1.1 2.2 |
|
| 214 |
1.1 |
|
| 219 |
1.1 |
|
| 263 |
1.1 |
|
| 264 |
1.1 2.2 |
|
| 271 |
1.1 2.2 3.3 |
|
| 290 |
1.1 2.2 |