1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.shared.storage; 18 19 import static com.android.adservices.shared.util.LogUtil.DEBUG; 20 import static com.android.adservices.shared.util.LogUtil.VERBOSE; 21 22 import android.annotation.Nullable; 23 import android.os.PersistableBundle; 24 import android.util.AtomicFile; 25 26 import com.android.adservices.shared.util.LogUtil; 27 import com.android.internal.annotations.GuardedBy; 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.util.Preconditions; 30 import com.android.modules.utils.build.SdkLevel; 31 32 import java.io.ByteArrayInputStream; 33 import java.io.ByteArrayOutputStream; 34 import java.io.File; 35 import java.io.FileNotFoundException; 36 import java.io.FileOutputStream; 37 import java.io.IOException; 38 import java.io.PrintWriter; 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.Map; 42 import java.util.Objects; 43 import java.util.Set; 44 import java.util.concurrent.locks.Lock; 45 import java.util.concurrent.locks.ReadWriteLock; 46 import java.util.concurrent.locks.ReentrantReadWriteLock; 47 import java.util.stream.Collectors; 48 49 /** 50 * A simple datastore utilizing {@link android.util.AtomicFile} and {@link 51 * android.os.PersistableBundle} to read/write a simple key/value map to file. 52 * 53 * <p>The datastore is loaded from file only when initialized and written to file on every write. 54 * When using this datastore, it is up to the caller to ensure that each datastore file is accessed 55 * by exactly one datastore object. If multiple writing threads or processes attempt to use 56 * different instances pointing to the same file, transactions may be lost. 57 * 58 * <p>Keys must be non-{@code null}, non-empty strings, and values must be booleans. 59 * 60 * @threadsafe 61 */ 62 public class BooleanFileDatastore { 63 public static final int NO_PREVIOUS_VERSION = -1; 64 65 private final int mDatastoreVersion; 66 67 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 68 private final Lock mReadLock = mReadWriteLock.readLock(); 69 private final Lock mWriteLock = mReadWriteLock.writeLock(); 70 71 private final AtomicFile mAtomicFile; 72 private final Map<String, Boolean> mLocalMap = new HashMap<>(); 73 74 private final String mVersionKey; 75 private int mPreviousStoredVersion; 76 BooleanFileDatastore( String parentPath, String filename, int datastoreVersion, String versionKey)77 public BooleanFileDatastore( 78 String parentPath, String filename, int datastoreVersion, String versionKey) { 79 this(newFile(parentPath, filename), datastoreVersion, versionKey); 80 } 81 BooleanFileDatastore(File file, int datastoreVersion, String versionKey)82 public BooleanFileDatastore(File file, int datastoreVersion, String versionKey) { 83 mAtomicFile = new AtomicFile(Objects.requireNonNull(file)); 84 mDatastoreVersion = 85 Preconditions.checkArgumentNonnegative( 86 datastoreVersion, "Version must not be negative"); 87 88 mVersionKey = Objects.requireNonNull(versionKey); 89 } 90 91 /** 92 * Loads data from the datastore file. 93 * 94 * @throws IOException if file read fails 95 */ initialize()96 public final void initialize() throws IOException { 97 if (DEBUG) { 98 LogUtil.d("Reading from store file: %s", mAtomicFile.getBaseFile()); 99 } 100 mReadLock.lock(); 101 try { 102 readFromFile(); 103 } finally { 104 mReadLock.unlock(); 105 } 106 107 // In the future, this could be a good place for upgrade/rollback for schemas 108 } 109 110 // Writes the class member map to a PersistableBundle which is then written to file. 111 @GuardedBy("mWriteLock") writeToFile()112 private void writeToFile() throws IOException { 113 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 114 final PersistableBundle bundleToWrite = new PersistableBundle(); 115 116 for (Map.Entry<String, Boolean> entry : mLocalMap.entrySet()) { 117 bundleToWrite.putBoolean(entry.getKey(), entry.getValue()); 118 } 119 120 // Version unused for now. May be needed in the future for handling migrations. 121 bundleToWrite.putInt(mVersionKey, mDatastoreVersion); 122 bundleToWrite.writeToStream(outputStream); 123 124 FileOutputStream out = null; 125 try { 126 out = mAtomicFile.startWrite(); 127 out.write(outputStream.toByteArray()); 128 mAtomicFile.finishWrite(out); 129 } catch (IOException e) { 130 if (out != null) { 131 mAtomicFile.failWrite(out); 132 } 133 LogUtil.e(e, "Write to file failed"); 134 throw e; 135 } 136 } 137 138 // Note that this completely replaces the loaded datastore with the file's data, instead of 139 // appending new file data. 140 @GuardedBy("mReadLock") readFromFile()141 private void readFromFile() throws IOException { 142 try { 143 final ByteArrayInputStream inputStream = 144 new ByteArrayInputStream(mAtomicFile.readFully()); 145 final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream); 146 147 mPreviousStoredVersion = bundleRead.getInt(mVersionKey, NO_PREVIOUS_VERSION); 148 bundleRead.remove(mVersionKey); 149 mLocalMap.clear(); 150 for (String key : bundleRead.keySet()) { 151 mLocalMap.put(key, bundleRead.getBoolean(key)); 152 } 153 } catch (FileNotFoundException e) { 154 if (VERBOSE) { 155 LogUtil.v("File not found; continuing with clear database"); 156 } 157 mPreviousStoredVersion = NO_PREVIOUS_VERSION; 158 mLocalMap.clear(); 159 } catch (IOException e) { 160 LogUtil.e(e, "Read from store file failed"); 161 throw e; 162 } 163 } 164 165 /** 166 * Stores a value to the datastore file. 167 * 168 * <p>This change is committed immediately to file. 169 * 170 * @param key A non-null, non-empty String key to store the {@code value} against 171 * @param value A boolean to be stored 172 * @throws IllegalArgumentException if {@code key} is an empty string 173 * @throws IOException if file write fails 174 * @throws NullPointerException if {@code key} is null 175 */ put(String key, boolean value)176 public final void put(String key, boolean value) throws IOException { 177 Objects.requireNonNull(key); 178 Preconditions.checkStringNotEmpty(key, "Key must not be empty"); 179 180 mWriteLock.lock(); 181 try { 182 mLocalMap.put(key, value); 183 writeToFile(); 184 } finally { 185 mWriteLock.unlock(); 186 } 187 } 188 189 /** 190 * Stores a value to the datastore file, but only if the key does not already exist. 191 * 192 * <p>If a change is made to the datastore, it is committed immediately to file. 193 * 194 * @param key A non-null, non-empty String key to store the {@code value} against 195 * @param value A boolean to be stored 196 * @return the value that exists in the datastore after the operation completes 197 * @throws IllegalArgumentException if {@code key} is an empty string 198 * @throws IOException if file write fails 199 * @throws NullPointerException if {@code key} is null 200 */ putIfNew(String key, boolean value)201 public final boolean putIfNew(String key, boolean value) throws IOException { 202 Objects.requireNonNull(key); 203 Preconditions.checkStringNotEmpty(key, "Key must not be empty"); 204 205 // Try not to block readers first before trying to write 206 mReadLock.lock(); 207 try { 208 Boolean valueInLocalMap = mLocalMap.get(key); 209 if (valueInLocalMap != null) { 210 return valueInLocalMap; 211 } 212 } finally { 213 mReadLock.unlock(); 214 } 215 216 // Double check that the key wasn't written after the first check 217 mWriteLock.lock(); 218 try { 219 Boolean valueInLocalMap = mLocalMap.get(key); 220 if (valueInLocalMap != null) { 221 return valueInLocalMap; 222 } else { 223 mLocalMap.put(key, value); 224 writeToFile(); 225 return value; 226 } 227 } finally { 228 mWriteLock.unlock(); 229 } 230 } 231 232 /** 233 * Retrieves a boolean value from the loaded datastore file. 234 * 235 * @param key A non-null, non-empty String key to fetch a value from 236 * @return The value stored against a {@code key}, or null if it doesn't exist 237 * @throws IllegalArgumentException if {@code key} is an empty string 238 * @throws NullPointerException if {@code key} is null 239 */ 240 @Nullable get(String key)241 public final Boolean get(String key) { 242 Objects.requireNonNull(key); 243 Preconditions.checkStringNotEmpty(key, "Key must not be empty"); 244 245 mReadLock.lock(); 246 try { 247 return mLocalMap.get(key); 248 } finally { 249 mReadLock.unlock(); 250 } 251 } 252 253 /** 254 * Retrieves a boolean value from the loaded datastore file. 255 * 256 * @param key A non-null, non-empty String key to fetch a value from 257 * @param defaultValue Value to return if this key does not exist. 258 * @throws IllegalArgumentException if {@code key} is an empty string 259 * @throws NullPointerException if {@code key} is null 260 */ 261 @Nullable get(String key, boolean defaultValue)262 public final Boolean get(String key, boolean defaultValue) { 263 Objects.requireNonNull(key); 264 Preconditions.checkStringNotEmpty(key, "Key must not be empty"); 265 266 mReadLock.lock(); 267 try { 268 return mLocalMap.containsKey(key) ? mLocalMap.get(key) : defaultValue; 269 } finally { 270 mReadLock.unlock(); 271 } 272 } 273 274 /** Returns the version that was written prior to the device starting. */ getPreviousStoredVersion()275 public final int getPreviousStoredVersion() { 276 return mPreviousStoredVersion; 277 } 278 279 /** 280 * Retrieves a {@link Set} of all keys loaded from the datastore file. 281 * 282 * @return A {@link Set} of {@link String} keys currently in the loaded datastore 283 */ keySet()284 public final Set<String> keySet() { 285 mReadLock.lock(); 286 try { 287 return getSafeSetCopy(mLocalMap.keySet()); 288 } finally { 289 mReadLock.unlock(); 290 } 291 } 292 keySetFilter(boolean filter)293 private Set<String> keySetFilter(boolean filter) { 294 mReadLock.lock(); 295 try { 296 return getSafeSetCopy( 297 mLocalMap.entrySet().stream() 298 .filter(entry -> entry.getValue().equals(filter)) 299 .map(Map.Entry::getKey) 300 .collect(Collectors.toSet())); 301 } finally { 302 mReadLock.unlock(); 303 } 304 } 305 306 /** 307 * Retrieves a Set of all keys with value {@code true} loaded from the datastore file. 308 * 309 * @return A Set of String keys currently in the loaded datastore that have value {@code true} 310 */ keySetTrue()311 public final Set<String> keySetTrue() { 312 return keySetFilter(true); 313 } 314 315 /** 316 * Retrieves a Set of all keys with value {@code false} loaded from the datastore file. 317 * 318 * @return A Set of String keys currently in the loaded datastore that have value {@code false} 319 */ keySetFalse()320 public final Set<String> keySetFalse() { 321 return keySetFilter(false); 322 } 323 324 /** Gets the version key. */ getVersionKey()325 public final String getVersionKey() { 326 return mVersionKey; 327 } 328 329 /** 330 * Clears all entries from the datastore file. 331 * 332 * <p>This change is committed immediately to file. 333 * 334 * @throws IOException if file write fails 335 */ clear()336 public final void clear() throws IOException { 337 if (DEBUG) { 338 LogUtil.d("Clearing all entries from datastore"); 339 } 340 341 mWriteLock.lock(); 342 try { 343 mLocalMap.clear(); 344 writeToFile(); 345 } finally { 346 mWriteLock.unlock(); 347 } 348 } 349 clearByFilter(boolean filter)350 private void clearByFilter(boolean filter) throws IOException { 351 mWriteLock.lock(); 352 try { 353 mLocalMap.entrySet().removeIf(entry -> entry.getValue().equals(filter)); 354 writeToFile(); 355 } finally { 356 mWriteLock.unlock(); 357 } 358 } 359 360 /** 361 * Clears all entries from the datastore file that have value {@code true}. Entries with value 362 * {@code false} are not removed. 363 * 364 * <p>This change is committed immediately to file. 365 * 366 * @throws IOException if file write fails 367 */ clearAllTrue()368 public void clearAllTrue() throws IOException { 369 clearByFilter(true); 370 } 371 372 /** 373 * Clears all entries from the datastore file that have value {@code false}. Entries with value 374 * {@code true} are not removed. 375 * 376 * <p>This change is committed immediately to file. 377 * 378 * @throws IOException if file write fails 379 */ clearAllFalse()380 public void clearAllFalse() throws IOException { 381 clearByFilter(false); 382 } 383 384 /** 385 * Removes an entry from the datastore file. 386 * 387 * <p>This change is committed immediately to file. 388 * 389 * @param key A non-null, non-empty String key to remove 390 * @throws IllegalArgumentException if {@code key} is an empty string 391 * @throws IOException if file write fails 392 * @throws NullPointerException if {@code key} is null 393 */ remove(String key)394 public void remove(String key) throws IOException { 395 Objects.requireNonNull(key); 396 Preconditions.checkStringNotEmpty(key, "Key must not be empty"); 397 398 mWriteLock.lock(); 399 try { 400 mLocalMap.remove(key); 401 writeToFile(); 402 } finally { 403 mWriteLock.unlock(); 404 } 405 } 406 407 /** 408 * Removes all entries that begin with the specified prefix from the datastore file. 409 * 410 * <p>This change is committed immediately to file. 411 * 412 * @param prefix A non-null, non-empty string that all keys are matched against 413 * @throws NullPointerException if {@code prefix} is null 414 * @throws IllegalArgumentException if {@code prefix} is an empty string 415 * @throws IOException if file write fails 416 */ removeByPrefix(String prefix)417 public void removeByPrefix(String prefix) throws IOException { 418 Objects.requireNonNull(prefix); 419 Preconditions.checkStringNotEmpty(prefix, "Prefix must not be empty"); 420 421 mWriteLock.lock(); 422 try { 423 Set<String> allKeys = mLocalMap.keySet(); 424 Set<String> keysToDelete = 425 allKeys.stream().filter(s -> s.startsWith(prefix)).collect(Collectors.toSet()); 426 allKeys.removeAll(keysToDelete); // Modifying the keySet updates the underlying map 427 writeToFile(); 428 } finally { 429 mWriteLock.unlock(); 430 } 431 } 432 433 /** Dumps its internal state. */ dump(PrintWriter writer, String prefix)434 public void dump(PrintWriter writer, String prefix) { 435 writer.printf("%smDatastoreVersion: %d\n", prefix, mDatastoreVersion); 436 writer.printf("%smPreviousStoredVersion: %d\n", prefix, mPreviousStoredVersion); 437 writer.printf("%smVersionKey: %s\n", prefix, mVersionKey); 438 writer.printf("%smAtomicFile: %s", prefix, mAtomicFile.getBaseFile().getAbsolutePath()); 439 if (SdkLevel.isAtLeastS()) { 440 writer.printf(" (last modified at %d)", mAtomicFile.getLastModifiedTime()); 441 } 442 int size = mLocalMap.size(); 443 writer.printf(":\n%s%d entries\n", prefix, size); 444 445 // TODO(b/299942046): decide whether it's ok to dump the entries themselves (perhaps passing 446 // an argument). 447 } 448 449 /** For tests only */ 450 @VisibleForTesting tearDownForTesting()451 public void tearDownForTesting() { 452 mWriteLock.lock(); 453 try { 454 mAtomicFile.delete(); 455 mLocalMap.clear(); 456 } finally { 457 mWriteLock.unlock(); 458 } 459 } 460 newFile(String parentPath, String filename)461 private static File newFile(String parentPath, String filename) { 462 Preconditions.checkStringNotEmpty(parentPath, "parentPath must not be empty or null"); 463 Preconditions.checkStringNotEmpty(filename, "filename must not be empty or null"); 464 File parent = new File(parentPath); 465 if (!parent.exists()) { 466 throw new IllegalArgumentException( 467 "parentPath doesn't exist: " + parent.getAbsolutePath()); 468 } 469 if (!parent.isDirectory()) { 470 throw new IllegalArgumentException( 471 "parentPath is not a directory: " + parent.getAbsolutePath()); 472 } 473 return new File(parent, filename); 474 } 475 476 // TODO(b/335869310): change it to using ImmutableSet. getSafeSetCopy(Set<T> sourceSet)477 private static <T> Set<T> getSafeSetCopy(Set<T> sourceSet) { 478 return new HashSet<>(sourceSet); 479 } 480 } 481