1 /* 2 * Copyright 2024 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 androidx.appsearch.localstorage; 18 19 import androidx.annotation.RestrictTo; 20 import androidx.annotation.VisibleForTesting; 21 import androidx.appsearch.exceptions.AppSearchException; 22 import androidx.appsearch.flags.Flags; 23 import androidx.appsearch.localstorage.util.PrefixUtil; 24 import androidx.collection.ArrayMap; 25 import androidx.collection.ArraySet; 26 import androidx.core.util.Preconditions; 27 28 import com.google.android.icing.proto.SchemaTypeConfigProto; 29 30 import org.jspecify.annotations.NonNull; 31 32 import java.util.ArrayDeque; 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Queue; 38 import java.util.Set; 39 40 /** 41 * Caches and manages schema information for AppSearch. 42 * 43 * @exportToFramework:hide 44 */ 45 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 46 public class SchemaCache { 47 /** 48 * A map that contains schema types and SchemaTypeConfigProtos for all package-database 49 * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 50 * prefixed schema type to its respective SchemaTypeConfigProto. 51 */ 52 private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap = new ArrayMap<>(); 53 54 /** 55 * A map that contains schema types and all children schema types for all package-database 56 * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 57 * prefixed schema type to its respective list of children prefixed schema types. 58 */ 59 private final Map<String, Map<String, List<String>>> mSchemaParentToChildrenMap = 60 new ArrayMap<>(); 61 62 /** 63 * A map that contains schema types and all parent schema types for all package-database 64 * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each 65 * prefixed schema type to its respective list of unprefixed parent schema types including 66 * transitive parents. It's guaranteed that child types always appear before parent types in 67 * the list. 68 */ 69 private final Map<String, Map<String, List<String>>> 70 mSchemaChildToTransitiveUnprefixedParentsMap = new ArrayMap<>(); 71 SchemaCache()72 public SchemaCache() { 73 } 74 75 @VisibleForTesting SchemaCache(@onNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)76 public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) 77 throws AppSearchException { 78 mSchemaMap.putAll(Preconditions.checkNotNull(schemaMap)); 79 rebuildCache(); 80 } 81 82 /** 83 * Returns the schema map for the given prefix. 84 */ getSchemaMapForPrefix( @onNull String prefix)85 public @NonNull Map<String, SchemaTypeConfigProto> getSchemaMapForPrefix( 86 @NonNull String prefix) { 87 Preconditions.checkNotNull(prefix); 88 89 Map<String, SchemaTypeConfigProto> schemaMap = mSchemaMap.get(prefix); 90 if (schemaMap == null) { 91 return Collections.emptyMap(); 92 } 93 return schemaMap; 94 } 95 96 /** 97 * Returns a set of all prefixes stored in the cache. 98 */ getAllPrefixes()99 public @NonNull Set<String> getAllPrefixes() { 100 return Collections.unmodifiableSet(mSchemaMap.keySet()); 101 } 102 103 /** 104 * Returns all prefixed schema types stored in the cache. 105 * 106 * <p>This method is inefficient to call repeatedly. 107 */ getAllPrefixedSchemaTypes()108 public @NonNull List<String> getAllPrefixedSchemaTypes() { 109 List<String> cachedPrefixedSchemaTypes = new ArrayList<>(); 110 for (Map<String, SchemaTypeConfigProto> value : mSchemaMap.values()) { 111 cachedPrefixedSchemaTypes.addAll(value.keySet()); 112 } 113 return cachedPrefixedSchemaTypes; 114 } 115 116 /** 117 * Returns the schema types for the given set of prefixed schema types with their 118 * descendants, based on the schema parent-to-children map held in the cache. 119 */ getSchemaTypesWithDescendants(@onNull String prefix, @NonNull Set<String> prefixedSchemaTypes)120 public @NonNull Set<String> getSchemaTypesWithDescendants(@NonNull String prefix, 121 @NonNull Set<String> prefixedSchemaTypes) { 122 Preconditions.checkNotNull(prefix); 123 Preconditions.checkNotNull(prefixedSchemaTypes); 124 Map<String, List<String>> parentToChildrenMap = mSchemaParentToChildrenMap.get(prefix); 125 if (parentToChildrenMap == null) { 126 parentToChildrenMap = Collections.emptyMap(); 127 } 128 129 // Perform a BFS search on the inheritance graph started by the set of prefixedSchemaTypes. 130 Set<String> visited = new ArraySet<>(); 131 Queue<String> prefixedSchemaQueue = new ArrayDeque<>(prefixedSchemaTypes); 132 while (!prefixedSchemaQueue.isEmpty()) { 133 String currentPrefixedSchema = prefixedSchemaQueue.poll(); 134 if (visited.contains(currentPrefixedSchema)) { 135 continue; 136 } 137 visited.add(currentPrefixedSchema); 138 List<String> children = parentToChildrenMap.get(currentPrefixedSchema); 139 if (children == null) { 140 continue; 141 } 142 prefixedSchemaQueue.addAll(children); 143 } 144 145 return visited; 146 } 147 148 /** 149 * Returns the unprefixed parent schema types, including transitive parents, for the given 150 * prefixed schema type, based on the schema child-to-parents map held in the cache. It's 151 * guaranteed that child types always appear before parent types in the list. 152 */ getTransitiveUnprefixedParentSchemaTypes(@onNull String prefix, @NonNull String prefixedSchemaType)153 public @NonNull List<String> getTransitiveUnprefixedParentSchemaTypes(@NonNull String prefix, 154 @NonNull String prefixedSchemaType) throws AppSearchException { 155 Preconditions.checkNotNull(prefix); 156 Preconditions.checkNotNull(prefixedSchemaType); 157 158 // If the flag is on, retrieve the parent types from the cache as it is available. 159 // Otherwise, recalculate the parent types. 160 if (Flags.enableSearchResultParentTypes()) { 161 Map<String, List<String>> unprefixedChildToParentsMap = 162 mSchemaChildToTransitiveUnprefixedParentsMap.get(prefix); 163 if (unprefixedChildToParentsMap == null) { 164 return Collections.emptyList(); 165 } 166 List<String> parents = unprefixedChildToParentsMap.get(prefixedSchemaType); 167 return parents == null ? Collections.emptyList() : parents; 168 } else { 169 return calculateTransitiveUnprefixedParentSchemaTypes(prefixedSchemaType, 170 getSchemaMapForPrefix(prefix)); 171 } 172 } 173 174 /** 175 * Rebuilds the schema parent-to-children and child-to-parents maps for the given prefix, 176 * based on the current schema map. 177 * 178 * <p>The schema parent-to-children and child-to-parents maps must be updated when 179 * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the 180 * results from {@link #getSchemaTypesWithDescendants} and 181 * {@link #getTransitiveUnprefixedParentSchemaTypes} would be stale. 182 */ rebuildCacheForPrefix(@onNull String prefix)183 public void rebuildCacheForPrefix(@NonNull String prefix) 184 throws AppSearchException { 185 Preconditions.checkNotNull(prefix); 186 187 mSchemaParentToChildrenMap.remove(prefix); 188 mSchemaChildToTransitiveUnprefixedParentsMap.remove(prefix); 189 Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMap.get(prefix); 190 if (prefixedSchemaMap == null) { 191 return; 192 } 193 194 // Build the parent-to-children map for the current prefix. 195 Map<String, List<String>> parentToChildrenMap = new ArrayMap<>(); 196 for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) { 197 for (int i = 0; i < childSchemaConfig.getParentTypesCount(); i++) { 198 String parent = childSchemaConfig.getParentTypes(i); 199 List<String> children = parentToChildrenMap.get(parent); 200 if (children == null) { 201 children = new ArrayList<>(); 202 parentToChildrenMap.put(parent, children); 203 } 204 children.add(childSchemaConfig.getSchemaType()); 205 } 206 } 207 // Record the map for the current prefix. 208 if (!parentToChildrenMap.isEmpty()) { 209 mSchemaParentToChildrenMap.put(prefix, parentToChildrenMap); 210 } 211 212 // If the flag is on, build the child-to-parent maps as caches. Otherwise, this 213 // information will have to be recalculated when needed. 214 if (Flags.enableSearchResultParentTypes()) { 215 // Build the child-to-parents maps for the current prefix. 216 Map<String, List<String>> childToTransitiveUnprefixedParentsMap = new ArrayMap<>(); 217 for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) { 218 if (childSchemaConfig.getParentTypesCount() > 0) { 219 childToTransitiveUnprefixedParentsMap.put( 220 childSchemaConfig.getSchemaType(), 221 calculateTransitiveUnprefixedParentSchemaTypes( 222 childSchemaConfig.getSchemaType(), 223 prefixedSchemaMap)); 224 } 225 } 226 // Record the map for the current prefix. 227 if (!childToTransitiveUnprefixedParentsMap.isEmpty()) { 228 mSchemaChildToTransitiveUnprefixedParentsMap.put(prefix, 229 childToTransitiveUnprefixedParentsMap); 230 } 231 } 232 } 233 234 /** 235 * Rebuilds the schema parent-to-children and child-to-parents maps based on the current 236 * schema map. 237 * 238 * <p>The schema parent-to-children and child-to-parents maps must be updated when 239 * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the 240 * results from {@link #getSchemaTypesWithDescendants} and 241 * {@link #getTransitiveUnprefixedParentSchemaTypes} would be stale. 242 */ rebuildCache()243 public void rebuildCache() throws AppSearchException { 244 mSchemaParentToChildrenMap.clear(); 245 mSchemaChildToTransitiveUnprefixedParentsMap.clear(); 246 for (String prefix : mSchemaMap.keySet()) { 247 rebuildCacheForPrefix(prefix); 248 } 249 } 250 251 /** 252 * Adds a schema to the schema map. 253 * 254 * <p>Note that this method will invalidate the schema parent-to-children and 255 * child-to-parents maps in the cache, and either {@link #rebuildCache} or 256 * {@link #rebuildCacheForPrefix} is required to be called to update the cache. 257 */ addToSchemaMap(@onNull String prefix, @NonNull SchemaTypeConfigProto schemaTypeConfigProto)258 public void addToSchemaMap(@NonNull String prefix, 259 @NonNull SchemaTypeConfigProto schemaTypeConfigProto) { 260 Preconditions.checkNotNull(prefix); 261 Preconditions.checkNotNull(schemaTypeConfigProto); 262 263 Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix); 264 if (schemaTypeMap == null) { 265 schemaTypeMap = new ArrayMap<>(); 266 mSchemaMap.put(prefix, schemaTypeMap); 267 } 268 schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto); 269 } 270 271 /** 272 * Removes a schema from the schema map. 273 * 274 * <p>Note that this method will invalidate the schema parent-to-children and 275 * child-to-parents maps in the cache, and either {@link #rebuildCache} or 276 * {@link #rebuildCacheForPrefix} is required to be called to update the cache. 277 */ removeFromSchemaMap(@onNull String prefix, @NonNull String schemaType)278 public void removeFromSchemaMap(@NonNull String prefix, @NonNull String schemaType) { 279 Preconditions.checkNotNull(prefix); 280 Preconditions.checkNotNull(schemaType); 281 282 Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix); 283 if (schemaTypeMap != null) { 284 schemaTypeMap.remove(schemaType); 285 } 286 } 287 288 /** 289 * Removes the entry of the given prefix from the schema map, the schema parent-to-children 290 * map and the child-to-parents map, and returns the set of removed prefixed schema type. 291 */ removePrefix(@onNull String prefix)292 public @NonNull Set<String> removePrefix(@NonNull String prefix) { 293 Preconditions.checkNotNull(prefix); 294 295 Map<String, SchemaTypeConfigProto> removedSchemas = 296 Preconditions.checkNotNull(mSchemaMap.remove(prefix)); 297 mSchemaParentToChildrenMap.remove(prefix); 298 mSchemaChildToTransitiveUnprefixedParentsMap.remove(prefix); 299 return removedSchemas.keySet(); 300 } 301 302 /** 303 * Clears all data in the cache. 304 */ clear()305 public void clear() { 306 mSchemaMap.clear(); 307 mSchemaParentToChildrenMap.clear(); 308 mSchemaChildToTransitiveUnprefixedParentsMap.clear(); 309 } 310 311 /** 312 * Get the list of unprefixed transitive parent type names of {@code prefixedSchemaType}. 313 * 314 * <p>It's guaranteed that child types always appear before parent types in the list. 315 */ calculateTransitiveUnprefixedParentSchemaTypes( @onNull String prefixedSchemaType, @NonNull Map<String, SchemaTypeConfigProto> prefixedSchemaMap)316 private @NonNull List<String> calculateTransitiveUnprefixedParentSchemaTypes( 317 @NonNull String prefixedSchemaType, 318 @NonNull Map<String, SchemaTypeConfigProto> prefixedSchemaMap) 319 throws AppSearchException { 320 // Please note that neither DFS nor BFS order is guaranteed to always put child types 321 // before parent types (due to the diamond problem), so a topological sorting algorithm 322 // is required. 323 Map<String, Integer> inDegreeMap = new ArrayMap<>(); 324 collectParentTypeInDegrees(prefixedSchemaType, prefixedSchemaMap, 325 /* visited= */new ArraySet<>(), inDegreeMap); 326 327 List<String> result = new ArrayList<>(); 328 Queue<String> queue = new ArrayDeque<>(); 329 // prefixedSchemaType is the only type that has zero in-degree at this point. 330 queue.add(prefixedSchemaType); 331 while (!queue.isEmpty()) { 332 SchemaTypeConfigProto currentSchema = Preconditions.checkNotNull( 333 prefixedSchemaMap.get(queue.poll())); 334 for (int i = 0; i < currentSchema.getParentTypesCount(); ++i) { 335 String prefixedParentType = currentSchema.getParentTypes(i); 336 int parentInDegree = 337 Preconditions.checkNotNull(inDegreeMap.get(prefixedParentType)) - 1; 338 inDegreeMap.put(prefixedParentType, parentInDegree); 339 if (parentInDegree == 0) { 340 result.add(PrefixUtil.removePrefix(prefixedParentType)); 341 queue.add(prefixedParentType); 342 } 343 } 344 } 345 return result; 346 } 347 collectParentTypeInDegrees( @onNull String prefixedSchemaType, @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap, @NonNull Set<String> visited, @NonNull Map<String, Integer> inDegreeMap)348 private void collectParentTypeInDegrees( 349 @NonNull String prefixedSchemaType, 350 @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap, 351 @NonNull Set<String> visited, @NonNull Map<String, Integer> inDegreeMap) { 352 if (visited.contains(prefixedSchemaType)) { 353 return; 354 } 355 visited.add(prefixedSchemaType); 356 SchemaTypeConfigProto schema = 357 Preconditions.checkNotNull(schemaTypeMap.get(prefixedSchemaType)); 358 for (int i = 0; i < schema.getParentTypesCount(); ++i) { 359 String prefixedParentType = schema.getParentTypes(i); 360 Integer parentInDegree = inDegreeMap.get(prefixedParentType); 361 if (parentInDegree == null) { 362 parentInDegree = 0; 363 } 364 inDegreeMap.put(prefixedParentType, parentInDegree + 1); 365 collectParentTypeInDegrees(prefixedParentType, schemaTypeMap, visited, inDegreeMap); 366 } 367 } 368 } 369