1 | package com.renomad.minum.database; | |
2 | ||
3 | import com.renomad.minum.state.Context; | |
4 | import com.renomad.minum.logging.ILogger; | |
5 | import com.renomad.minum.queue.AbstractActionQueue; | |
6 | import com.renomad.minum.queue.ActionQueue; | |
7 | import com.renomad.minum.utils.FileUtils; | |
8 | ||
9 | import java.io.BufferedReader; | |
10 | import java.io.FileReader; | |
11 | import java.io.IOException; | |
12 | import java.nio.charset.StandardCharsets; | |
13 | import java.nio.file.Files; | |
14 | import java.nio.file.Path; | |
15 | import java.util.*; | |
16 | import java.util.concurrent.Callable; | |
17 | import java.util.concurrent.ConcurrentHashMap; | |
18 | import java.util.concurrent.atomic.AtomicLong; | |
19 | import java.util.concurrent.locks.Lock; | |
20 | import java.util.concurrent.locks.ReentrantLock; | |
21 | import java.util.function.Function; | |
22 | ||
23 | import static com.renomad.minum.utils.Invariants.*; | |
24 | ||
25 | /** | |
26 | * a memory-based disk-persisted database class. | |
27 | * @param <T> the type of data we'll be persisting (must extend from {@link DbData} | |
28 | */ | |
29 | public final class Db<T extends DbData<?>> { | |
30 | ||
31 | /** | |
32 | * The suffix we will apply to each database file | |
33 | */ | |
34 | static final String DATABASE_FILE_SUFFIX = ".ddps"; | |
35 | private final T emptyInstance; | |
36 | ||
37 | private final Lock loadDataLock = new ReentrantLock(); | |
38 | ||
39 | /** | |
40 | * The full path to the file that contains the most-recent index | |
41 | * for this data. As we add new files, each gets its own index | |
42 | * value. When we start the program, we use this to determine | |
43 | * where to start counting for new indexes. | |
44 | */ | |
45 | private final Path fullPathForIndexFile; | |
46 | ||
47 | final AtomicLong index; | |
48 | ||
49 | /** | |
50 | * An in-memory representation of the value of the current max index | |
51 | * that we store in index.ddps, in memory, so we can compare whether | |
52 | * we need to update the disk without checking the disk so often. | |
53 | */ | |
54 | private long maxIndexOnDisk; | |
55 | ||
56 | private final Path dbDirectory; | |
57 | private final AbstractActionQueue actionQueue; | |
58 | private final ILogger logger; | |
59 | private final Map<Long, T> data; | |
60 | private final FileUtils fileUtils; | |
61 | private boolean hasLoadedData; | |
62 | ||
63 | // components for registered indexes (for faster read performance) | |
64 | private final Map<String, Map<String, Set<T>>> registeredIndexes; | |
65 | private final Map<String, Function<T, String>> partitioningMap; | |
66 | ||
67 | /** | |
68 | * Constructs an in-memory disk-persisted database. | |
69 | * Loading of data from disk happens at the first invocation of any command | |
70 | * changing or requesting data, such as {@link #write(DbData)}, {@link #delete(DbData)}, | |
71 | * or {@link #values()}. See the private method loadData() for details. | |
72 | * @param dbDirectory this uniquely names your database, and also sets the directory | |
73 | * name for this data. The expected use case is to name this after | |
74 | * the data in question. For example, "users", or "accounts". | |
75 | * @param context used to provide important state data to several components | |
76 | * @param instance an instance of the {@link DbData} object relevant for use in this database. Note | |
77 | * that each database (that is, each instance of this class), focuses on just one | |
78 | * data, which must be an implementation of {@link DbData}. | |
79 | */ | |
80 | public Db(Path dbDirectory, Context context, T instance) { | |
81 | this.hasLoadedData = false; | |
82 | data = new ConcurrentHashMap<>(); | |
83 | actionQueue = new ActionQueue("DatabaseWriter " + dbDirectory, context).initialize(); | |
84 | this.logger = context.getLogger(); | |
85 | this.dbDirectory = dbDirectory; | |
86 | this.fullPathForIndexFile = dbDirectory.resolve("index" + DATABASE_FILE_SUFFIX); | |
87 | this.emptyInstance = instance; | |
88 | this.fileUtils = new FileUtils(logger, context.getConstants()); | |
89 | this.registeredIndexes = new HashMap<>(); | |
90 | this.partitioningMap = new HashMap<>(); | |
91 | ||
92 |
1
1. <init> : negated conditional → KILLED |
if (Files.exists(fullPathForIndexFile)) { |
93 | long indexValue; | |
94 | try (var fileReader = new FileReader(fullPathForIndexFile.toFile(), StandardCharsets.UTF_8)) { | |
95 | try (BufferedReader br = new BufferedReader(fileReader)) { | |
96 | String s = br.readLine(); | |
97 |
1
1. <init> : negated conditional → KILLED |
if (s == null) throw new DbException("index file for " + dbDirectory + " returned null when reading a line from it"); |
98 | mustBeFalse(s.isBlank(), "Unless something is terribly broken, we expect a numeric value here"); | |
99 | String trim = s.trim(); | |
100 | indexValue = Long.parseLong(trim); | |
101 | } | |
102 | } catch (Exception e) { | |
103 | throw new DbException("Exception while reading "+fullPathForIndexFile+" in Db constructor", e); | |
104 | } | |
105 | ||
106 | this.index = new AtomicLong(indexValue); | |
107 | ||
108 | } else { | |
109 | this.index = new AtomicLong(1); | |
110 | } | |
111 | ||
112 |
2
1. <init> : removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED 2. lambda$new$0 : removed call to com/renomad/minum/utils/FileUtils::makeDirectory → KILLED |
actionQueue.enqueue("create directory" + dbDirectory, () -> fileUtils.makeDirectory(dbDirectory)); |
113 | } | |
114 | ||
115 | /** | |
116 | * This function will stop the minum.database persistence cleanly. | |
117 | * <p> | |
118 | * In order to do this, we need to wait for our threads | |
119 | * to finish their work. In particular, we | |
120 | * have offloaded our file writes to [actionQueue], which | |
121 | * has an internal thread for serializing all actions | |
122 | * on our minum.database | |
123 | * </p> | |
124 | */ | |
125 | public void stop() { | |
126 |
1
1. stop : removed call to com/renomad/minum/queue/AbstractActionQueue::stop → KILLED |
actionQueue.stop(); |
127 | } | |
128 | ||
129 | /** | |
130 | * Similar to {@link #stop()} but gives more control over how long | |
131 | * we'll wait before crashing it closed. See {@link ActionQueue#stop(int, int)} | |
132 | */ | |
133 | public void stop(int count, int sleepTime) { | |
134 |
1
1. stop : removed call to com/renomad/minum/queue/AbstractActionQueue::stop → KILLED |
actionQueue.stop(count, sleepTime); |
135 | } | |
136 | ||
137 | /** | |
138 | * Write data to the database. Use an index of 0 to store new data, and a positive | |
139 | * non-zero value to update data. | |
140 | * <p><em> | |
141 | * Example of adding new data to the database: | |
142 | * </p></em> | |
143 | * {@snippet : | |
144 | * final var newSalt = StringUtils.generateSecureRandomString(10); | |
145 | * final var hashedPassword = CryptoUtils.createPasswordHash(newPassword, newSalt); | |
146 | * final var newUser = new User(0L, newUsername, hashedPassword, newSalt); | |
147 | * userDb.write(newUser); | |
148 | * } | |
149 | * <p><em> | |
150 | * Example of updating data: | |
151 | * </p></em> | |
152 | * {@snippet : | |
153 | * // write the updated salted password to the database | |
154 | * final var updatedUser = new User( | |
155 | * user().getIndex(), | |
156 | * user().getUsername(), | |
157 | * hashedPassword, | |
158 | * newSalt); | |
159 | * userDb.write(updatedUser); | |
160 | * } | |
161 | * | |
162 | * @param newData the data we are writing | |
163 | */ | |
164 | public T write(T newData) { | |
165 |
2
1. write : negated conditional → KILLED 2. write : changed conditional boundary → KILLED |
if (newData.getIndex() < 0) throw new DbException("Negative indexes are disallowed"); |
166 | // load data if needed | |
167 |
2
1. write : negated conditional → KILLED 2. write : removed call to com/renomad/minum/database/Db::loadData → KILLED |
if (!hasLoadedData) loadData(); |
168 | ||
169 |
1
1. write : removed call to com/renomad/minum/database/Db::writeToMemory → KILLED |
writeToMemory(newData); |
170 | ||
171 | // *** now handle the disk portion *** | |
172 |
2
1. lambda$write$1 : removed call to com/renomad/minum/database/Db::writeToDisk → KILLED 2. write : removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED |
actionQueue.enqueue("persist data to disk", () -> writeToDisk(newData)); |
173 | ||
174 | // returning the data at this point is the most convenient | |
175 | // way users will have access to the new index of the data. | |
176 |
1
1. write : replaced return value with null for com/renomad/minum/database/Db::write → KILLED |
return newData; |
177 | } | |
178 | ||
179 | /** | |
180 | * Write database data into memory | |
181 | * @param newData the new data may be totally new or an update | |
182 | */ | |
183 | private void writeToMemory(T newData) { | |
184 | // *** deal with the in-memory portion *** | |
185 | boolean newElementCreated = false; | |
186 | // create a new index for the data, if needed | |
187 |
1
1. writeToMemory : negated conditional → KILLED |
if (newData.getIndex() == 0) { |
188 |
1
1. writeToMemory : removed call to com/renomad/minum/database/DbData::setIndex → KILLED |
newData.setIndex(index.getAndIncrement()); |
189 | newElementCreated = true; | |
190 | } else { | |
191 | // if the data does not exist, and a positive non-zero | |
192 | // index was provided, throw an exception. | |
193 |
2
1. lambda$writeToMemory$2 : negated conditional → KILLED 2. lambda$writeToMemory$2 : replaced boolean return with true for com/renomad/minum/database/Db::lambda$writeToMemory$2 → KILLED |
boolean dataEntryExists = data.values().stream().anyMatch(x -> x.getIndex() == newData.getIndex()); |
194 |
1
1. writeToMemory : negated conditional → KILLED |
if (!dataEntryExists) { |
195 | throw new DbException( | |
196 | String.format("Positive indexes are only allowed when updating existing data. Index: %d", | |
197 | newData.getIndex())); | |
198 | } | |
199 | } | |
200 | // if we got here, we are safe to proceed with putting the data into memory and disk | |
201 | logger.logTrace(() -> String.format("in thread %s, writing data %s", Thread.currentThread().getName(), newData)); | |
202 | T oldData = data.put(newData.getIndex(), newData); | |
203 | ||
204 | // handle the indexes differently depending on whether this is a create or delete | |
205 |
1
1. writeToMemory : negated conditional → KILLED |
if (newElementCreated) { |
206 |
1
1. writeToMemory : removed call to com/renomad/minum/database/Db::addToIndexes → KILLED |
addToIndexes(newData); |
207 | } else { | |
208 |
1
1. writeToMemory : removed call to com/renomad/minum/database/Db::removeFromIndexes → KILLED |
removeFromIndexes(oldData); |
209 |
1
1. writeToMemory : removed call to com/renomad/minum/database/Db::addToIndexes → KILLED |
addToIndexes(newData); |
210 | } | |
211 | } | |
212 | ||
213 | private void writeToDisk(T newData) { | |
214 | final Path fullPath = dbDirectory.resolve(newData.getIndex() + DATABASE_FILE_SUFFIX); | |
215 | logger.logTrace(() -> String.format("writing data to %s", fullPath)); | |
216 | String serializedData = newData.serialize(); | |
217 | mustBeFalse(serializedData == null || serializedData.isBlank(), | |
218 | "the serialized form of data must not be blank. " + | |
219 | "Is the serialization code written properly? Our datatype: " + emptyInstance); | |
220 |
1
1. writeToDisk : removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED |
fileUtils.writeString(fullPath, serializedData); |
221 |
2
1. writeToDisk : changed conditional boundary → TIMED_OUT 2. writeToDisk : negated conditional → KILLED |
if (maxIndexOnDisk < index.get()) { |
222 | maxIndexOnDisk = index.get(); | |
223 |
1
1. writeToDisk : removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED |
fileUtils.writeString(fullPathForIndexFile, String.valueOf(maxIndexOnDisk)); |
224 | } | |
225 | } | |
226 | ||
227 | /** | |
228 | * Delete data | |
229 | * <p><em>Example:</p></em> | |
230 | * {@snippet : | |
231 | * userDb.delete(user); | |
232 | * } | |
233 | * @param dataToDelete the data we are serializing and writing | |
234 | */ | |
235 | public void delete(T dataToDelete) { | |
236 | // load data if needed | |
237 |
2
1. delete : negated conditional → KILLED 2. delete : removed call to com/renomad/minum/database/Db::loadData → KILLED |
if (!hasLoadedData) loadData(); |
238 | ||
239 | // deal with the in-memory portion | |
240 |
1
1. delete : removed call to com/renomad/minum/database/Db::deleteFromMemory → KILLED |
deleteFromMemory(dataToDelete); |
241 | ||
242 | // now handle the disk portion | |
243 |
2
1. delete : removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED 2. lambda$delete$5 : removed call to com/renomad/minum/database/Db::deleteFromDisk → KILLED |
actionQueue.enqueue("delete data from disk", () -> deleteFromDisk(dataToDelete.getIndex())); |
244 | } | |
245 | ||
246 | private void deleteFromDisk(long dataIndexToDelete) { | |
247 | final Path fullPath = dbDirectory.resolve(dataIndexToDelete + DATABASE_FILE_SUFFIX); | |
248 | logger.logTrace(() -> String.format("deleting data at %s", fullPath)); | |
249 | try { | |
250 |
1
1. deleteFromDisk : negated conditional → KILLED |
if (!fullPath.toFile().exists()) { |
251 | throw new DbException(fullPath + " must already exist before deletion"); | |
252 | } | |
253 |
1
1. deleteFromDisk : removed call to java/nio/file/Files::delete → KILLED |
Files.delete(fullPath); |
254 |
2
1. deleteFromDisk : changed conditional boundary → TIMED_OUT 2. deleteFromDisk : negated conditional → TIMED_OUT |
if (maxIndexOnDisk > index.get()) { |
255 | maxIndexOnDisk = index.get(); | |
256 |
1
1. deleteFromDisk : removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED |
fileUtils.writeString(fullPathForIndexFile, String.valueOf(maxIndexOnDisk)); |
257 | ||
258 | } | |
259 | } catch (Exception ex) { | |
260 | logger.logAsyncError(() -> "failed to delete file " + fullPath + " during deleteOnDisk. Exception: " + ex); | |
261 | } | |
262 | } | |
263 | ||
264 | private void deleteFromMemory(T dataToDelete) { | |
265 | long dataIndex; | |
266 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (dataToDelete == null) { |
267 | throw new DbException("Invalid to be given a null value to delete"); | |
268 | } | |
269 | dataIndex = dataToDelete.getIndex(); | |
270 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (!data.containsKey(dataIndex)) { |
271 | throw new DbException("no data was found with index of " + dataIndex); | |
272 | } | |
273 | long finalDataIndex = dataIndex; | |
274 | logger.logTrace(() -> String.format("in thread %s, deleting data with index %d", Thread.currentThread().getName(), finalDataIndex)); | |
275 | data.remove(dataIndex); | |
276 |
1
1. deleteFromMemory : removed call to com/renomad/minum/database/Db::removeFromIndexes → KILLED |
removeFromIndexes(dataToDelete); |
277 | // if all the data was just now deleted, we need to | |
278 | // reset the index back to 1 | |
279 | ||
280 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (data.isEmpty()) { |
281 |
1
1. deleteFromMemory : removed call to java/util/concurrent/atomic/AtomicLong::set → TIMED_OUT |
index.set(1); |
282 | } | |
283 | } | |
284 | ||
285 | /** | |
286 | * Grabs all the data from disk and returns it as a list. This | |
287 | * method is run by various programs when the system first loads. | |
288 | */ | |
289 | void loadDataFromDisk() { | |
290 |
1
1. loadDataFromDisk : negated conditional → KILLED |
if (! Files.exists(dbDirectory)) { |
291 | logger.logDebug(() -> dbDirectory + " directory missing, adding nothing to the data list"); | |
292 | return; | |
293 | } | |
294 | ||
295 |
1
1. loadDataFromDisk : removed call to com/renomad/minum/database/Db::walkAndLoad → KILLED |
walkAndLoad(dbDirectory); |
296 | } | |
297 | ||
298 | void walkAndLoad(Path dbDirectory) { | |
299 | // walk through all the files in this directory, collecting | |
300 | // all regular files (non-subdirectories) except for index.ddps | |
301 | try (final var pathStream = Files.walk(dbDirectory)) { | |
302 | final var listOfFiles = pathStream.filter(path -> | |
303 |
2
1. lambda$walkAndLoad$10 : replaced boolean return with true for com/renomad/minum/database/Db::lambda$walkAndLoad$10 → KILLED 2. lambda$walkAndLoad$10 : negated conditional → KILLED |
Files.isRegularFile(path) && |
304 |
1
1. lambda$walkAndLoad$10 : negated conditional → KILLED |
!path.getFileName().toString().startsWith("index") |
305 | ).toList(); | |
306 | for (Path p : listOfFiles) { | |
307 |
1
1. walkAndLoad : removed call to com/renomad/minum/database/Db::readAndDeserialize → KILLED |
readAndDeserialize(p); |
308 | } | |
309 | } catch (IOException e) { | |
310 | throw new DbException(e); | |
311 | } | |
312 | } | |
313 | ||
314 | /** | |
315 | * Carry out the process of reading data files into our in-memory structure | |
316 | * @param p the path of a particular file | |
317 | */ | |
318 | void readAndDeserialize(Path p) throws IOException { | |
319 | Path fileName = p.getFileName(); | |
320 |
1
1. readAndDeserialize : negated conditional → KILLED |
if (fileName == null) throw new DbException("At readAndDeserialize, path " + p + " returned a null filename"); |
321 | String filename = fileName.toString(); | |
322 | int startOfSuffixIndex = filename.indexOf('.'); | |
323 |
1
1. readAndDeserialize : negated conditional → KILLED |
if(startOfSuffixIndex == -1) { |
324 | throw new DbException("the files must have a ddps suffix, like 1.ddps. filename: " + filename); | |
325 | } | |
326 | String fileContents = Files.readString(p); | |
327 |
1
1. readAndDeserialize : negated conditional → KILLED |
if (fileContents.isBlank()) { |
328 | logger.logDebug( () -> fileName + " file exists but empty, skipping"); | |
329 | } else { | |
330 | try { | |
331 | @SuppressWarnings("unchecked") | |
332 | T deserializedData = (T) emptyInstance.deserialize(fileContents); | |
333 | mustBeTrue(deserializedData != null, "deserialization of " + emptyInstance + | |
334 | " resulted in a null value. Was the serialization method implemented properly?"); | |
335 | int fileNameIdentifier = Integer.parseInt(filename.substring(0, startOfSuffixIndex)); | |
336 | mustBeTrue(deserializedData.getIndex() == fileNameIdentifier, | |
337 | "The filename must correspond to the data's index. e.g. 1.ddps must have an id of 1"); | |
338 | ||
339 | // put the data into the in-memory data structure | |
340 | data.put(deserializedData.getIndex(), deserializedData); | |
341 |
1
1. readAndDeserialize : removed call to com/renomad/minum/database/Db::addToIndexes → KILLED |
addToIndexes(deserializedData); |
342 | ||
343 | } catch (Exception e) { | |
344 | throw new DbException("Failed to deserialize "+ p +" with data (\""+fileContents+"\"). Caused by: " + e); | |
345 | } | |
346 | } | |
347 | } | |
348 | ||
349 | private void addToIndexes(T dbData) { | |
350 | // add the data to registered indexes. For each of the registered indexes, | |
351 | // get the stored function to obtain a string value which helps divide | |
352 | // the overall data into partitions. | |
353 | for (var entry : partitioningMap.entrySet()) { | |
354 | // a function provided by the user to obtain an index-key: a unique or semi-unique | |
355 | // value to help partition / index the data | |
356 | Function<T, String> indexStringFunction = entry.getValue(); | |
357 | String propertyAsString = indexStringFunction.apply(dbData); | |
358 | Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey()); | |
359 | synchronized (this) { | |
360 |
1
1. lambda$addToIndexes$12 : replaced return value with Collections.emptySet for com/renomad/minum/database/Db::lambda$addToIndexes$12 → KILLED |
stringIndexMap.computeIfAbsent(propertyAsString, k -> new HashSet<>()); |
361 | } | |
362 | // if the index-key provides a 1-to-1 mapping to items, like UUIDs, then | |
363 | // each value will have only one item in the collection. In other cases, | |
364 | // like when partitioning the data into multiple groups, there could easily | |
365 | // be many items per index value. | |
366 | Set<T> dataSet = stringIndexMap.get(propertyAsString); | |
367 | dataSet.add(dbData); | |
368 | } | |
369 | } | |
370 | ||
371 | /** | |
372 | * Run when an item is deleted from the database | |
373 | */ | |
374 | private void removeFromIndexes(T dbData) { | |
375 | for (var entry : partitioningMap.entrySet()) { | |
376 | // a function provided by the user to obtain an index-key: a unique or semi-unique | |
377 | // value to help partition / index the data | |
378 | Function<T, String> indexStringFunction = entry.getValue(); | |
379 | String propertyAsString = indexStringFunction.apply(dbData); | |
380 | Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey()); | |
381 | synchronized (this) { | |
382 |
2
1. lambda$removeFromIndexes$13 : negated conditional → KILLED 2. lambda$removeFromIndexes$13 : replaced boolean return with true for com/renomad/minum/database/Db::lambda$removeFromIndexes$13 → KILLED |
stringIndexMap.get(propertyAsString).removeIf(x -> x.getIndex() == dbData.getIndex()); |
383 | ||
384 | // in certain cases, we're removing one of the items that is indexed but | |
385 | // there are more left. If there's nothing left though, we'll remove the mapping. | |
386 |
1
1. removeFromIndexes : negated conditional → KILLED |
if (stringIndexMap.get(propertyAsString).isEmpty()) { |
387 | stringIndexMap.remove(propertyAsString); | |
388 | } | |
389 | } | |
390 | } | |
391 | } | |
392 | ||
393 | /** | |
394 | * This method provides read capability for the values of a database. | |
395 | * <br> | |
396 | * The returned collection is a read-only view over the data, through {@link Collections#unmodifiableCollection(Collection)} | |
397 | * | |
398 | * <p><em>Example:</em></p> | |
399 | * {@snippet : | |
400 | * boolean doesUserAlreadyExist(String username) { | |
401 | * return userDb.values().stream().anyMatch(x -> x.getUsername().equals(username)); | |
402 | * } | |
403 | * } | |
404 | */ | |
405 | public Collection<T> values() { | |
406 | // load data if needed | |
407 |
2
1. values : removed call to com/renomad/minum/database/Db::loadData → TIMED_OUT 2. values : negated conditional → KILLED |
if (!hasLoadedData) loadData(); |
408 | ||
409 |
1
1. values : replaced return value with Collections.emptyList for com/renomad/minum/database/Db::values → KILLED |
return Collections.unmodifiableCollection(data.values()); |
410 | } | |
411 | ||
412 | /** | |
413 | * This is what loads the data from disk the | |
414 | * first time someone needs it. Because it is | |
415 | * locked, only one thread can enter at | |
416 | * a time. The first one in will load the data, | |
417 | * and the second will encounter a branch which skips loading. | |
418 | */ | |
419 | private void loadData() { | |
420 |
1
1. loadData : removed call to java/util/concurrent/locks/Lock::lock → KILLED |
loadDataLock.lock(); // block threads here if multiple are trying to get in - only one gets in at a time |
421 | try { | |
422 |
1
1. loadData : removed call to com/renomad/minum/database/Db::loadDataCore → KILLED |
loadDataCore(hasLoadedData, this::loadDataFromDisk); |
423 | hasLoadedData = true; | |
424 | } finally { | |
425 |
1
1. loadData : removed call to java/util/concurrent/locks/Lock::unlock → KILLED |
loadDataLock.unlock(); |
426 | } | |
427 | } | |
428 | ||
429 | static void loadDataCore(boolean hasLoadedData, Runnable loadDataFromDisk) { | |
430 |
1
1. loadDataCore : negated conditional → KILLED |
if (!hasLoadedData) { |
431 |
1
1. loadDataCore : removed call to java/lang/Runnable::run → KILLED |
loadDataFromDisk.run(); |
432 | } | |
433 | } | |
434 | ||
435 | /** | |
436 | * Register an index in the database for higher performance data access | |
437 | * @param indexName a string used to distinguish this index. This string will be used again | |
438 | * when requesting data in a method like {@link #getIndexedData} or {@link #findExactlyOne} | |
439 | * @param keyObtainingFunction a function which obtains data from the data in this database, used | |
440 | * to partition the data into groups (potentially up to a 1-to-1 correspondence | |
441 | * between id and object) | |
442 | * @return true if the registration succeeded | |
443 | */ | |
444 | public boolean registerIndex(String indexName, Function<T, String> keyObtainingFunction) { | |
445 |
1
1. registerIndex : negated conditional → KILLED |
if (keyObtainingFunction == null) { |
446 | throw new DbException("When registering an index, the partitioning algorithm must not be null"); | |
447 | } | |
448 |
2
1. registerIndex : negated conditional → KILLED 2. registerIndex : negated conditional → KILLED |
if (indexName == null || indexName.isBlank()) { |
449 | throw new DbException("When registering an index, value must be a non-empty string"); | |
450 | } | |
451 |
1
1. registerIndex : negated conditional → KILLED |
if (registeredIndexes.containsKey(indexName)) { |
452 | throw new DbException("It is forbidden to register the same index more than once. Duplicate index: \""+indexName+"\""); | |
453 | } | |
454 | HashMap<String, Set<T>> stringCollectionHashMap = new HashMap<>(); | |
455 | registeredIndexes.put(indexName, stringCollectionHashMap); | |
456 | partitioningMap.put(indexName, keyObtainingFunction); | |
457 |
1
1. registerIndex : replaced boolean return with false for com/renomad/minum/database/Db::registerIndex → KILLED |
return true; |
458 | } | |
459 | ||
460 | /** | |
461 | * Given the name of a registered index (see {@link #registerIndex(String, Function)}), | |
462 | * use the key to find the collection of data that matches it. | |
463 | * @param indexName the name of an index | |
464 | * @param key a string value that matches a partition calculated from the partition | |
465 | * function provided to {@link #registerIndex(String, Function)} | |
466 | * @return a collection of data, an empty collection if nothing found | |
467 | */ | |
468 | public Collection<T> getIndexedData(String indexName, String key) { | |
469 |
1
1. getIndexedData : negated conditional → KILLED |
if (!registeredIndexes.containsKey(indexName)) { |
470 | throw new DbException("There is no index registered on the database Db<"+this.emptyInstance.getClass().getSimpleName()+"> with a name of \""+indexName+"\""); | |
471 | } | |
472 | Set<T> values = registeredIndexes.get(indexName).get(key); | |
473 | // return an empty set rather than null | |
474 |
1
1. getIndexedData : replaced return value with Collections.emptyList for com/renomad/minum/database/Db::getIndexedData → KILLED |
return Objects.requireNonNullElseGet(values, Set::of); |
475 | } | |
476 | ||
477 | /** | |
478 | * Get a set of the currently-registered indexes on this database, useful | |
479 | * for debugging. | |
480 | */ | |
481 | public Set<String> getSetOfIndexes() { | |
482 |
1
1. getSetOfIndexes : replaced return value with Collections.emptySet for com/renomad/minum/database/Db::getSetOfIndexes → KILLED |
return partitioningMap.keySet(); |
483 | } | |
484 | ||
485 | /** | |
486 | * A utility to find exactly one item from the database. | |
487 | * <br> | |
488 | * This utility will search the indexes for a particular data by | |
489 | * indexName and indexKey. If not found, it will return null. If | |
490 | * found, it will be returned. If more than one are found, an exception | |
491 | * will be thrown. Use this tool when the data has been uniquely | |
492 | * indexed, like for example when setting a unique identifier into | |
493 | * each data. | |
494 | * @param indexName the name of the index, an arbitrary value set by the | |
495 | * user to help distinguish among potentially many indexes | |
496 | * set on this data | |
497 | * @param indexKey the key for this particular value, such as a UUID or a name | |
498 | * or any other way to partition the data | |
499 | * @see #findExactlyOne(String, String, Callable) | |
500 | */ | |
501 | public T findExactlyOne(String indexName, String indexKey) { | |
502 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED |
return findExactlyOne(indexName, indexKey, () -> null); |
503 | } | |
504 | ||
505 | /** | |
506 | * Find one item, with an alternate value if null | |
507 | * <br> | |
508 | * This utility will search the indexes for a particular data by | |
509 | * indexName and indexKey. If not found, it will return null. If | |
510 | * found, it will be returned. If more than one are found, an exception | |
511 | * will be thrown. Use this tool when the data has been uniquely | |
512 | * indexed, like for example when setting a unique identifier into | |
513 | * each data. | |
514 | * @param indexName the name of the index, an arbitrary value set by the | |
515 | * user to help distinguish among potentially many indexes | |
516 | * set on this data | |
517 | * @param indexKey the key for this particular value, such as a UUID or a name | |
518 | * or any other way to partition the data | |
519 | * @param alternate a functional interface that will be run if the result would | |
520 | * have been null, useful for situations where you don't want | |
521 | * the output to be null when nothing is found. | |
522 | * @see #findExactlyOne(String, String) | |
523 | */ | |
524 | public T findExactlyOne(String indexName, String indexKey, Callable<T> alternate) { | |
525 | Collection<T> indexedData = getIndexedData(indexName, indexKey); | |
526 |
1
1. findExactlyOne : negated conditional → KILLED |
if (indexedData.isEmpty()) { |
527 | try { | |
528 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED |
return alternate.call(); |
529 | } catch (Exception ex) { | |
530 | throw new DbException(ex); | |
531 | } | |
532 |
1
1. findExactlyOne : negated conditional → KILLED |
} else if (indexedData.size() == 1) { |
533 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED |
return indexedData.stream().findFirst().orElseThrow(); |
534 | } else { | |
535 | throw new DbException("More than one item found when searching database Db<%s> on index \"%s\" with key %s" | |
536 | .formatted(emptyInstance.getClass().getSimpleName(), indexName, indexKey)); | |
537 | } | |
538 | } | |
539 | } | |
Mutations | ||
92 |
1.1 |
|
97 |
1.1 |
|
112 |
1.1 2.2 |
|
126 |
1.1 |
|
134 |
1.1 |
|
165 |
1.1 2.2 |
|
167 |
1.1 2.2 |
|
169 |
1.1 |
|
172 |
1.1 2.2 |
|
176 |
1.1 |
|
187 |
1.1 |
|
188 |
1.1 |
|
193 |
1.1 2.2 |
|
194 |
1.1 |
|
205 |
1.1 |
|
206 |
1.1 |
|
208 |
1.1 |
|
209 |
1.1 |
|
220 |
1.1 |
|
221 |
1.1 2.2 |
|
223 |
1.1 |
|
237 |
1.1 2.2 |
|
240 |
1.1 |
|
243 |
1.1 2.2 |
|
250 |
1.1 |
|
253 |
1.1 |
|
254 |
1.1 2.2 |
|
256 |
1.1 |
|
266 |
1.1 |
|
270 |
1.1 |
|
276 |
1.1 |
|
280 |
1.1 |
|
281 |
1.1 |
|
290 |
1.1 |
|
295 |
1.1 |
|
303 |
1.1 2.2 |
|
304 |
1.1 |
|
307 |
1.1 |
|
320 |
1.1 |
|
323 |
1.1 |
|
327 |
1.1 |
|
341 |
1.1 |
|
360 |
1.1 |
|
382 |
1.1 2.2 |
|
386 |
1.1 |
|
407 |
1.1 2.2 |
|
409 |
1.1 |
|
420 |
1.1 |
|
422 |
1.1 |
|
425 |
1.1 |
|
430 |
1.1 |
|
431 |
1.1 |
|
445 |
1.1 |
|
448 |
1.1 2.2 |
|
451 |
1.1 |
|
457 |
1.1 |
|
469 |
1.1 |
|
474 |
1.1 |
|
482 |
1.1 |
|
502 |
1.1 |
|
526 |
1.1 |
|
528 |
1.1 |
|
532 |
1.1 |
|
533 |
1.1 |