| 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.IOException; | |
| 8 | import java.nio.file.Path; | |
| 9 | import java.util.*; | |
| 10 | import java.util.concurrent.Callable; | |
| 11 | import java.util.concurrent.ConcurrentHashMap; | |
| 12 | import java.util.concurrent.atomic.AtomicLong; | |
| 13 | import java.util.concurrent.locks.ReentrantLock; | |
| 14 | import java.util.function.Function; | |
| 15 | ||
| 16 | /** | |
| 17 | * The abstract database class is a representation of the essential capabilities of | |
| 18 | * a Minum database. | |
| 19 | * <p> | |
| 20 | * There are two kinds of database provided, which only differ in how they | |
| 21 | * store data on disk. The "classic" kind, {@link Db}, stores each piece of | |
| 22 | * data in its own file. This is the simplest approach. | |
| 23 | * </p> | |
| 24 | * <p> | |
| 25 | * However, for significant speed gains, the new {@link DbEngine2} will | |
| 26 | * store each change as an append to a file, and will consolidate the on-disk | |
| 27 | * data occasionally, and on start. That way is thousands of times faster | |
| 28 | * to write to disk and to read from disk at startup. | |
| 29 | * </p> | |
| 30 | * @param <T> This is the type of data, which is always an implementation of | |
| 31 | * the {@link DbData} class. See the code of {@link com.renomad.minum.security.Inmate} | |
| 32 | * for an example of how this should look. | |
| 33 | */ | |
| 34 | public abstract class AbstractDb<T extends DbData<?>> { | |
| 35 | ||
| 36 | /** | |
| 37 | * The directory of the database on disk | |
| 38 | */ | |
| 39 | protected final Path dbDirectory; | |
| 40 | ||
| 41 | /** | |
| 42 | * An empty instance of the type of data stored by this | |
| 43 | * database, used for better handling of generics. | |
| 44 | */ | |
| 45 | protected final T emptyInstance; | |
| 46 | ||
| 47 | /** | |
| 48 | * Used for handling some file utilities in the database like creating directories | |
| 49 | */ | |
| 50 | protected final IFileUtils fileUtils; | |
| 51 | ||
| 52 | /** | |
| 53 | * Holds some system-wide information that is beneficial for components of the database | |
| 54 | */ | |
| 55 | protected final Context context; | |
| 56 | ||
| 57 | /** | |
| 58 | * Used for providing logging throughout the database | |
| 59 | */ | |
| 60 | protected final ILogger logger; | |
| 61 | ||
| 62 | /** | |
| 63 | * The internal data structure of the database that resides in memory. The beating heart | |
| 64 | * of the database while it runs. | |
| 65 | */ | |
| 66 | protected final Map<Long, T> data; | |
| 67 | ||
| 68 | /** | |
| 69 | * used to place locks around certain actions that need to avoid | |
| 70 | * thread interleaving. | |
| 71 | */ | |
| 72 | private final ReentrantLock dbLock; | |
| 73 | ||
| 74 | /** | |
| 75 | * The current index, used when creating new data items. Each item has its own | |
| 76 | * index value, this is where it is tracked. | |
| 77 | */ | |
| 78 | protected AtomicLong index; | |
| 79 | ||
| 80 | // components for registered indexes (for faster read performance) | |
| 81 | ||
| 82 | /** | |
| 83 | * This data structure is a nested map used for providing indexed data search. | |
| 84 | * <br> | |
| 85 | * The outer map is between the name of the index and the inner map. | |
| 86 | * <br> | |
| 87 | * The inner map is between strings and sets of items related to that string. | |
| 88 | */ | |
| 89 | protected final Map<String, Map<String, Set<T>>> registeredIndexes; | |
| 90 | ||
| 91 | /** | |
| 92 | * This map holds the functions that are registered to indexes, which are used | |
| 93 | * to construct the mappings between string values and items in the database. | |
| 94 | */ | |
| 95 | protected final Map<String, Function<T, String>> partitioningMap; | |
| 96 | ||
| 97 | protected AbstractDb(Path dbDirectory, Context context, T instance, IFileUtils fileUtils) { | |
| 98 | this.dbDirectory = dbDirectory; | |
| 99 | this.context = context; | |
| 100 | this.emptyInstance = instance; | |
| 101 | this.data = new ConcurrentHashMap<>(); | |
| 102 | this.logger = context.getLogger(); | |
| 103 | this.registeredIndexes = new HashMap<>(); | |
| 104 | this.partitioningMap = new HashMap<>(); | |
| 105 | this.fileUtils = fileUtils; | |
| 106 | this.dbLock = new ReentrantLock(); | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Used to cleanly stop the database. | |
| 111 | * <br> | |
| 112 | * In the case of {@link Db} this will interrupt its internal queue and tell it | |
| 113 | * to finish up processing. | |
| 114 | * <br> | |
| 115 | * In the case of {@link DbEngine2} this will flush data to disk. | |
| 116 | */ | |
| 117 | public abstract void stop() throws IOException; | |
| 118 | ||
| 119 | /** | |
| 120 | * Used to cleanly stop the database, with extra allowance of time | |
| 121 | * for cleanup. | |
| 122 | * <br> | |
| 123 | * Note that this method mostly applies to {@link Db}, and not as much | |
| 124 | * to {@link DbEngine2}. Only Db uses a processing queue on a thread which | |
| 125 | * is what requires a longer shutdown time for interruption. | |
| 126 | * @param count number of loops before we are done waiting for a clean close | |
| 127 | * and instead crash the instance closed. | |
| 128 | * @param sleepTime how long to wait, in milliseconds, for each iteration of the waiting loop. | |
| 129 | */ | |
| 130 | public abstract void stop(int count, int sleepTime) throws IOException; | |
| 131 | ||
| 132 | ||
| 133 | /** | |
| 134 | * Write data to the database. Use an index of 0 to store new data, and a positive | |
| 135 | * non-zero value to update data. | |
| 136 | * <p><em> | |
| 137 | * Example of adding new data to the database: | |
| 138 | * </em></p> | |
| 139 | * {@snippet : | |
| 140 | * final var newSalt = StringUtils.generateSecureRandomString(10); | |
| 141 | * final var hashedPassword = CryptoUtils.createPasswordHash(newPassword, newSalt); | |
| 142 | * final var newUser = new User(0L, newUsername, hashedPassword, newSalt); | |
| 143 | * userDb.write(newUser); | |
| 144 | * } | |
| 145 | * <p><em> | |
| 146 | * Example of updating data: | |
| 147 | * </em></p> | |
| 148 | * {@snippet : | |
| 149 | * // write the updated salted password to the database | |
| 150 | * final var updatedUser = new User( | |
| 151 | * user().getIndex(), | |
| 152 | * user().getUsername(), | |
| 153 | * hashedPassword, | |
| 154 | * newSalt); | |
| 155 | * userDb.write(updatedUser); | |
| 156 | * } | |
| 157 | * | |
| 158 | * @param newData the data we are writing | |
| 159 | * @return the data with its new index assigned. | |
| 160 | */ | |
| 161 | public abstract T write(T newData); | |
| 162 | ||
| 163 | /** | |
| 164 | * Write database data into memory | |
| 165 | * @param newData the new data may be totally new or an update | |
| 166 | * @param newElementCreated if true, this is a create. If false, an update. | |
| 167 | */ | |
| 168 | protected void writeToMemory(T newData, boolean newElementCreated) { | |
| 169 | // if we got here, we are safe to proceed with putting the data into memory and disk | |
| 170 | logger.logTrace(() -> String.format("in thread %s, writing data %s", Thread.currentThread().getName(), newData)); | |
| 171 | T oldData = data.put(newData.getIndex(), newData); | |
| 172 | ||
| 173 | // handle the indexes differently depending on whether this is a create or delete | |
| 174 |
1
1. writeToMemory : negated conditional → KILLED |
if (newElementCreated) { |
| 175 |
1
1. writeToMemory : removed call to com/renomad/minum/database/AbstractDb::addToIndexes → KILLED |
addToIndexes(newData); |
| 176 | } else { | |
| 177 |
1
1. writeToMemory : removed call to com/renomad/minum/database/AbstractDb::removeFromIndexes → KILLED |
removeFromIndexes(oldData); |
| 178 |
1
1. writeToMemory : removed call to com/renomad/minum/database/AbstractDb::addToIndexes → KILLED |
addToIndexes(newData); |
| 179 | } | |
| 180 | } | |
| 181 | ||
| 182 | /** | |
| 183 | * When new data comes in, we look at its "index" value. If | |
| 184 | * it is zero, it's a create, and we assign it a new value. If it is | |
| 185 | * positive, it is an update, and we had better find it in the database | |
| 186 | * already, or else throw an exception. | |
| 187 | * @return true if a create, false if an update | |
| 188 | */ | |
| 189 | protected boolean processDataIndex(T newData) { | |
| 190 | // *** deal with the in-memory portion *** | |
| 191 | boolean newElementCreated = false; | |
| 192 | // create a new index for the data, if needed | |
| 193 |
1
1. processDataIndex : negated conditional → KILLED |
if (newData.getIndex() == 0) { |
| 194 |
1
1. processDataIndex : removed call to com/renomad/minum/database/DbData::setIndex → KILLED |
newData.setIndex(index.getAndIncrement()); |
| 195 | newElementCreated = true; | |
| 196 | } else { | |
| 197 | // if the data does not exist, and a positive non-zero | |
| 198 | // index was provided, throw an exception. | |
| 199 | boolean dataEntryExists = data.containsKey(newData.getIndex()); | |
| 200 |
1
1. processDataIndex : negated conditional → KILLED |
if (!dataEntryExists) { |
| 201 | throw new DbException( | |
| 202 | String.format("Positive indexes are only allowed when updating existing data. Index: %d", | |
| 203 | newData.getIndex())); | |
| 204 | } | |
| 205 | } | |
| 206 |
2
1. processDataIndex : replaced boolean return with false for com/renomad/minum/database/AbstractDb::processDataIndex → TIMED_OUT 2. processDataIndex : replaced boolean return with true for com/renomad/minum/database/AbstractDb::processDataIndex → KILLED |
return newElementCreated; |
| 207 | } | |
| 208 | ||
| 209 | /** | |
| 210 | * Delete data | |
| 211 | * <p><em>Example:</em></p> | |
| 212 | * {@snippet : | |
| 213 | * userDb.delete(user); | |
| 214 | * } | |
| 215 | * | |
| 216 | * @param dataToDelete the data we are serializing and writing | |
| 217 | */ | |
| 218 | public abstract void delete(T dataToDelete); | |
| 219 | ||
| 220 | ||
| 221 | /** | |
| 222 | * Remove a particular item from the internal data structure in memory | |
| 223 | */ | |
| 224 | protected void deleteFromMemory(T dataToDelete) { | |
| 225 | long dataIndex; | |
| 226 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (dataToDelete == null) { |
| 227 | throw new DbException("Invalid to be given a null value to delete"); | |
| 228 | } | |
| 229 | dataIndex = dataToDelete.getIndex(); | |
| 230 |
1
1. deleteFromMemory : negated conditional → TIMED_OUT |
if (!data.containsKey(dataIndex)) { |
| 231 | throw new DbException("no data was found with index of " + dataIndex); | |
| 232 | } | |
| 233 | long finalDataIndex = dataIndex; | |
| 234 | logger.logTrace(() -> String.format("in thread %s, deleting data with index %d", Thread.currentThread().getName(), finalDataIndex)); | |
| 235 | data.remove(dataIndex); | |
| 236 |
1
1. deleteFromMemory : removed call to com/renomad/minum/database/AbstractDb::removeFromIndexes → TIMED_OUT |
removeFromIndexes(dataToDelete); |
| 237 | ||
| 238 | // if all the data was just now deleted, we need to | |
| 239 | // reset the index back to 1 | |
| 240 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (data.isEmpty()) { |
| 241 |
1
1. deleteFromMemory : removed call to java/util/concurrent/atomic/AtomicLong::set → KILLED |
index.set(1); |
| 242 | } | |
| 243 | } | |
| 244 | ||
| 245 | ||
| 246 | /** | |
| 247 | * add the data to registered indexes. | |
| 248 | * <br> | |
| 249 | * For each of the registered indexes, | |
| 250 | * get the stored function to obtain a string value which helps divide | |
| 251 | * the overall data into partitions. | |
| 252 | */ | |
| 253 | protected void addToIndexes(T dbData) { | |
| 254 | ||
| 255 | for (var entry : partitioningMap.entrySet()) { | |
| 256 | // a function provided by the user to obtain an index-key: a unique or semi-unique | |
| 257 | // value to help partition / index the data | |
| 258 | Function<T, String> indexStringFunction = entry.getValue(); | |
| 259 | String propertyAsString = indexStringFunction.apply(dbData); | |
| 260 | Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey()); | |
| 261 |
1
1. addToIndexes : removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED |
dbLock.lock(); |
| 262 | try { | |
| 263 |
1
1. lambda$addToIndexes$2 : replaced return value with Collections.emptySet for com/renomad/minum/database/AbstractDb::lambda$addToIndexes$2 → KILLED |
stringIndexMap.computeIfAbsent(propertyAsString, k -> new HashSet<>()); |
| 264 | // if the index-key provides a 1-to-1 mapping to items, like UUIDs, then | |
| 265 | // each value will have only one item in the collection. In other cases, | |
| 266 | // like when partitioning the data into multiple groups, there could easily | |
| 267 | // be many items per index value. | |
| 268 | Set<T> dataSet = stringIndexMap.get(propertyAsString); | |
| 269 | dataSet.add(dbData); | |
| 270 | } finally { | |
| 271 |
1
1. addToIndexes : removed call to java/util/concurrent/locks/ReentrantLock::unlock → KILLED |
dbLock.unlock(); |
| 272 | } | |
| 273 | } | |
| 274 | } | |
| 275 | ||
| 276 | /** | |
| 277 | * Run when an item is deleted from the database | |
| 278 | */ | |
| 279 | private void removeFromIndexes(T dbData) { | |
| 280 | for (var entry : partitioningMap.entrySet()) { | |
| 281 | // a function provided by the user to obtain an index-key: a unique or semi-unique | |
| 282 | // value to help partition / index the data | |
| 283 | Function<T, String> indexStringFunction = entry.getValue(); | |
| 284 | String propertyAsString = indexStringFunction.apply(dbData); | |
| 285 | Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey()); | |
| 286 |
1
1. removeFromIndexes : removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED |
dbLock.lock(); |
| 287 | try { | |
| 288 |
2
1. lambda$removeFromIndexes$3 : replaced boolean return with true for com/renomad/minum/database/AbstractDb::lambda$removeFromIndexes$3 → KILLED 2. lambda$removeFromIndexes$3 : negated conditional → KILLED |
stringIndexMap.get(propertyAsString).removeIf(x -> x.getIndex() == dbData.getIndex()); |
| 289 | ||
| 290 | // in certain cases, we're removing one of the items that is indexed but | |
| 291 | // there are more left. If there's nothing left though, we'll remove the mapping. | |
| 292 |
1
1. removeFromIndexes : negated conditional → TIMED_OUT |
if (stringIndexMap.get(propertyAsString).isEmpty()) { |
| 293 | stringIndexMap.remove(propertyAsString); | |
| 294 | } | |
| 295 | } finally { | |
| 296 |
1
1. removeFromIndexes : removed call to java/util/concurrent/locks/ReentrantLock::unlock → TIMED_OUT |
dbLock.unlock(); |
| 297 | } | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | ||
| 302 | /** | |
| 303 | * Cause the database to immediately load all its data. | |
| 304 | * <p> | |
| 305 | * If this method is not invoked by the developer, then the data will be loaded | |
| 306 | * lazily, at the first point where it is needed, such as when | |
| 307 | * getting or writing data. | |
| 308 | * </p> | |
| 309 | * <p> | |
| 310 | * It is a good idea to regularly use this method at database | |
| 311 | * initialization. By doing so, the check for data corruption issues will | |
| 312 | * throw an exception that will cause the whole application to halt | |
| 313 | * immediately, which is far preferable to having the exception show | |
| 314 | * as a complaint in the logs. | |
| 315 | * </p> | |
| 316 | * <p> | |
| 317 | * An example database initialization with bells and whistles | |
| 318 | * is as follows: | |
| 319 | * </p> | |
| 320 | * <pre> | |
| 321 | * {@code | |
| 322 | * AbstractDb<PersonName> sampleDomainDb = context.getDb2("names", PersonName.EMPTY) | |
| 323 | * .registerIndex("name_index", name -> name) | |
| 324 | * .loadData(); | |
| 325 | * } | |
| 326 | * </pre> | |
| 327 | */ | |
| 328 | public abstract AbstractDb<T> loadData(); | |
| 329 | ||
| 330 | /** | |
| 331 | * This method returns a read-only view of the values of a database. | |
| 332 | * | |
| 333 | * <p><em>Example:</em></p> | |
| 334 | * {@snippet : | |
| 335 | * boolean doesUserAlreadyExist(String username) { | |
| 336 | * return userDb.values().stream().anyMatch(x -> x.getUsername().equals(username)); | |
| 337 | * } | |
| 338 | * } | |
| 339 | */ | |
| 340 | public abstract Collection<T> values(); | |
| 341 | ||
| 342 | /** | |
| 343 | * Register an index in the database for higher performance data access. | |
| 344 | * <p> | |
| 345 | * This command should be run immediately after database declaration, | |
| 346 | * or more specifically, before any data is loaded from disk. Otherwise, | |
| 347 | * it would be possible to skip indexing that data. | |
| 348 | * </p> | |
| 349 | * <br> | |
| 350 | * Example: | |
| 351 | * <pre> | |
| 352 | * {@code | |
| 353 | * final var myDatabase = context.getDb("photos", Photograph.EMPTY) | |
| 354 | * .registerIndex("url", photo -> photo.getUrl()) | |
| 355 | * .loadData(); | |
| 356 | * } | |
| 357 | * </pre> | |
| 358 | * @param indexName a string used to distinguish this index. This string will be used again | |
| 359 | * when requesting data in a method like {@link #getIndexedData} or {@link #findExactlyOne} | |
| 360 | * @param keyObtainingFunction a function which obtains data from the data in this database, used | |
| 361 | * to partition the data into groups (potentially up to a 1-to-1 correspondence | |
| 362 | * between id and object) | |
| 363 | * @return the database instance if the registration succeeded | |
| 364 | * @throws DbException if the parameters are not entered properly, if the index has already | |
| 365 | * been registered, or if the data has already been loaded. It is necessary that | |
| 366 | * this is run immediately after declaring the database. To explain further: the data is not | |
| 367 | * actually loaded until the first time it is needed, such as running a write or delete, or | |
| 368 | * if the {@link #loadData()} ()} method is run. Creating an index map for the data that | |
| 369 | * is read from disk only occurs once, at data load time. Thus, it is crucial that the | |
| 370 | * registerIndex command is run before any data is loaded. | |
| 371 | */ | |
| 372 | public AbstractDb<T> registerIndex(String indexName, Function<T, String> keyObtainingFunction) { | |
| 373 |
1
1. registerIndex : negated conditional → KILLED |
if (keyObtainingFunction == null) { |
| 374 | throw new DbException("When registering an index, the partitioning algorithm must not be null"); | |
| 375 | } | |
| 376 |
2
1. registerIndex : negated conditional → KILLED 2. registerIndex : negated conditional → KILLED |
if (indexName == null || indexName.isBlank()) { |
| 377 | throw new DbException("When registering an index, value must be a non-empty string"); | |
| 378 | } | |
| 379 |
1
1. registerIndex : negated conditional → KILLED |
if (registeredIndexes.containsKey(indexName)) { |
| 380 | throw new DbException("It is forbidden to register the same index more than once. Duplicate index: \""+indexName+"\""); | |
| 381 | } | |
| 382 | HashMap<String, Set<T>> stringCollectionHashMap = new HashMap<>(); | |
| 383 | registeredIndexes.put(indexName, stringCollectionHashMap); | |
| 384 | partitioningMap.put(indexName, keyObtainingFunction); | |
| 385 |
1
1. registerIndex : replaced return value with null for com/renomad/minum/database/AbstractDb::registerIndex → KILLED |
return this; |
| 386 | } | |
| 387 | ||
| 388 | /** | |
| 389 | * Given the name of a registered index (see {@link #registerIndex(String, Function)}), | |
| 390 | * use the key to find the collection of data that matches it. | |
| 391 | * @param indexName the name of an index | |
| 392 | * @param key a string value that matches a partition calculated from the partition | |
| 393 | * function provided to {@link #registerIndex(String, Function)} | |
| 394 | * @return a collection of data, an empty collection if nothing found | |
| 395 | */ | |
| 396 | public Collection<T> getIndexedData(String indexName, String key) { | |
| 397 |
1
1. getIndexedData : negated conditional → KILLED |
if (!registeredIndexes.containsKey(indexName)) { |
| 398 | throw new DbException("There is no index registered on the database Db<"+this.emptyInstance.getClass().getSimpleName()+"> with a name of \""+indexName+"\""); | |
| 399 | } | |
| 400 |
1
1. getIndexedData : removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED |
dbLock.lock(); |
| 401 | try { | |
| 402 | Set<T> ts = registeredIndexes.get(indexName).get(key); | |
| 403 |
1
1. getIndexedData : negated conditional → TIMED_OUT |
if (ts != null) { |
| 404 |
1
1. getIndexedData : replaced return value with Collections.emptyList for com/renomad/minum/database/AbstractDb::getIndexedData → KILLED |
return new HashSet<>(ts); |
| 405 | } else { | |
| 406 | return Set.of(); | |
| 407 | } | |
| 408 | } finally { | |
| 409 |
1
1. getIndexedData : removed call to java/util/concurrent/locks/ReentrantLock::unlock → TIMED_OUT |
dbLock.unlock(); |
| 410 | } | |
| 411 | } | |
| 412 | ||
| 413 | /** | |
| 414 | * Get a set of the currently-registered indexes on this database, useful | |
| 415 | * for debugging. | |
| 416 | */ | |
| 417 | public Set<String> getSetOfIndexes() { | |
| 418 |
1
1. getSetOfIndexes : replaced return value with Collections.emptySet for com/renomad/minum/database/AbstractDb::getSetOfIndexes → KILLED |
return partitioningMap.keySet(); |
| 419 | } | |
| 420 | ||
| 421 | /** | |
| 422 | * A utility to find exactly one item from the database. | |
| 423 | * <br> | |
| 424 | * This utility will search the indexes for a particular data by | |
| 425 | * indexName and indexKey. If not found, it will return null. If | |
| 426 | * found, it will be returned. If more than one are found, an exception | |
| 427 | * will be thrown. Use this tool when the data has been uniquely | |
| 428 | * indexed, like for example when setting a unique identifier into | |
| 429 | * each data. | |
| 430 | * @param indexName the name of the index, an arbitrary value set by the | |
| 431 | * user to help distinguish among potentially many indexes | |
| 432 | * set on this data | |
| 433 | * @param indexKey the key for this particular value, such as a UUID or a name | |
| 434 | * or any other way to partition the data | |
| 435 | * @see #findExactlyOne(String, String, Callable) | |
| 436 | */ | |
| 437 | public T findExactlyOne(String indexName, String indexKey) { | |
| 438 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/AbstractDb::findExactlyOne → TIMED_OUT |
return findExactlyOne(indexName, indexKey, () -> null); |
| 439 | } | |
| 440 | ||
| 441 | /** | |
| 442 | * Find one item, with an alternate value if nothing was found | |
| 443 | * <br> | |
| 444 | * This utility will search the indexes for a particular data by | |
| 445 | * indexName and indexKey. If not found, it will return an alternate | |
| 446 | * value provided by the "alternate" parameter. If | |
| 447 | * found, it will be returned. If more than one are found, an exception | |
| 448 | * will be thrown. Use this tool when the data has been uniquely | |
| 449 | * indexed, like for example when setting a unique identifier into | |
| 450 | * each data. | |
| 451 | * @param indexName the name of the index, an arbitrary value set by the | |
| 452 | * user to help distinguish among potentially many indexes | |
| 453 | * set on this data | |
| 454 | * @param indexKey the key for this particular value, such as a UUID or a name | |
| 455 | * or any other way to partition the data | |
| 456 | * @param alternate a functional interface that will be run if no result | |
| 457 | * was found | |
| 458 | * @see #findExactlyOne(String, String) | |
| 459 | */ | |
| 460 | public T findExactlyOne(String indexName, String indexKey, Callable<T> alternate) { | |
| 461 | Collection<T> indexedData = getIndexedData(indexName, indexKey); | |
| 462 |
1
1. findExactlyOne : negated conditional → KILLED |
if (indexedData.isEmpty()) { |
| 463 | try { | |
| 464 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/AbstractDb::findExactlyOne → TIMED_OUT |
return alternate.call(); |
| 465 | } catch (Exception ex) { | |
| 466 | throw new DbException(ex); | |
| 467 | } | |
| 468 |
1
1. findExactlyOne : negated conditional → KILLED |
} else if (indexedData.size() == 1) { |
| 469 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/AbstractDb::findExactlyOne → KILLED |
return indexedData.stream().findFirst().orElseThrow(); |
| 470 | } else { | |
| 471 | throw new DbException("More than one item found when searching database Db<%s> on index \"%s\" with key %s" | |
| 472 | .formatted(emptyInstance.getClass().getSimpleName(), indexName, indexKey)); | |
| 473 | } | |
| 474 | } | |
| 475 | } | |
Mutations | ||
| 174 |
1.1 |
|
| 175 |
1.1 |
|
| 177 |
1.1 |
|
| 178 |
1.1 |
|
| 193 |
1.1 |
|
| 194 |
1.1 |
|
| 200 |
1.1 |
|
| 206 |
1.1 2.2 |
|
| 226 |
1.1 |
|
| 230 |
1.1 |
|
| 236 |
1.1 |
|
| 240 |
1.1 |
|
| 241 |
1.1 |
|
| 261 |
1.1 |
|
| 263 |
1.1 |
|
| 271 |
1.1 |
|
| 286 |
1.1 |
|
| 288 |
1.1 2.2 |
|
| 292 |
1.1 |
|
| 296 |
1.1 |
|
| 373 |
1.1 |
|
| 376 |
1.1 2.2 |
|
| 379 |
1.1 |
|
| 385 |
1.1 |
|
| 397 |
1.1 |
|
| 400 |
1.1 |
|
| 403 |
1.1 |
|
| 404 |
1.1 |
|
| 409 |
1.1 |
|
| 418 |
1.1 |
|
| 438 |
1.1 |
|
| 462 |
1.1 |
|
| 464 |
1.1 |
|
| 468 |
1.1 |
|
| 469 |
1.1 |