1 /* 2 * Copyright 2023 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 // @exportToFramework:skipFile() 17 package androidx.appsearch.app; 18 19 import android.util.Log; 20 21 import androidx.annotation.AnyThread; 22 import androidx.annotation.GuardedBy; 23 import androidx.annotation.RestrictTo; 24 import androidx.annotation.WorkerThread; 25 import androidx.appsearch.annotation.Document; 26 import androidx.collection.ArrayMap; 27 28 import org.jspecify.annotations.NonNull; 29 import org.jspecify.annotations.Nullable; 30 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Objects; 36 import java.util.ServiceLoader; 37 38 /** 39 * A class that maintains the map from schema type names to the fully qualified names of the 40 * corresponding document classes. 41 */ 42 @AnyThread 43 public abstract class AppSearchDocumentClassMap { 44 45 private static final String TAG = "AppSearchDocumentClassM"; 46 private static final Object sLock = new Object(); 47 48 /** 49 * The cached value of {@link #getGlobalMap()}. 50 */ 51 private static volatile Map<String, List<String>> sGlobalMap = null; 52 53 /** 54 * The cached value of {@code Class.forName(className)} for AppSearch document classes. 55 */ 56 private static volatile Map<String, Class<?>> sCachedAppSearchClasses = new ArrayMap<>(); 57 58 /** 59 * Returns the global map that includes all AppSearch document classes annotated with 60 * {@link Document} that are available in the current runtime. It maps from AppSearch's type 61 * name specified by {@link Document#name()} to the list of the fully qualified names of the 62 * corresponding document classes. The values are lists because it is possible that two 63 * document classes are associated with the same AppSearch type name. 64 * 65 * <p>Note that although this method, under normal circumstances, executes quickly, it 66 * performs a synchronous disk read operation in order to build the map, which means it can 67 * potentially introduce I/O blocking if executed on the main thread. 68 * 69 * <p>Since every call to this method should return the same map, the value of this map will 70 * be internally cached, so that only the first call will perform disk I/O. 71 */ 72 @WorkerThread getGlobalMap()73 public static @NonNull Map<String, List<String>> getGlobalMap() { 74 if (sGlobalMap == null) { 75 synchronized (sLock) { 76 if (sGlobalMap == null) { 77 sGlobalMap = buildGlobalMapLocked(); 78 } 79 } 80 } 81 return sGlobalMap; 82 } 83 84 /** 85 * Looks up the provided map to find a class for {@code schemaName} that is assignable to 86 * {@code documentClass}. Returns null if such class is not found. 87 */ 88 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) getAssignableClassBySchemaName( @onNull Map<String, List<String>> map, @NonNull String schemaName, @NonNull Class<T> documentClass)89 public static <T> @Nullable Class<? extends T> getAssignableClassBySchemaName( 90 @NonNull Map<String, List<String>> map, @NonNull String schemaName, 91 @NonNull Class<T> documentClass) { 92 List<String> classNames = map.get(schemaName); 93 if (classNames == null) { 94 return null; 95 } 96 // If there are multiple classes that correspond to the schema name, then we will: 97 // 1. skip any classes that are not assignable to documentClass. 98 // 2. if there are still multiple candidates, return the first one in the global map. 99 for (int i = 0; i < classNames.size(); ++i) { 100 String className = classNames.get(i); 101 try { 102 Class<?> clazz = getAppSearchDocumentClass(className); 103 if (documentClass.isAssignableFrom(clazz)) { 104 return clazz.asSubclass(documentClass); 105 } 106 } catch (ClassNotFoundException e) { 107 Log.w(TAG, "Failed to load document class \"" + className + "\". Perhaps the " 108 + "class was proguarded out?"); 109 } 110 } 111 return null; 112 } 113 114 /** 115 * Returns the map from schema type names to the list of the fully qualified names of the 116 * corresponding document classes. 117 */ getMap()118 protected abstract @NonNull Map<String, List<String>> getMap(); 119 getAppSearchDocumentClass(@onNull String className)120 private static @NonNull Class<?> getAppSearchDocumentClass(@NonNull String className) 121 throws ClassNotFoundException { 122 Class<?> result; 123 synchronized (sLock) { 124 result = sCachedAppSearchClasses.get(className); 125 } 126 if (result == null) { 127 result = Class.forName(className); 128 synchronized (sLock) { 129 sCachedAppSearchClasses.put(className, result); 130 } 131 } 132 return result; 133 } 134 135 /** 136 * Collects all of the instances of the generated {@link AppSearchDocumentClassMap} classes 137 * available in the current JVM environment, and calls the {@link #getMap()} method from them to 138 * build and return the merged map. The keys are schema type names, and the values are the 139 * lists of the corresponding document classes. 140 */ 141 @GuardedBy("AppSearchDocumentClassMap.sLock") buildGlobalMapLocked()142 private static @NonNull Map<String, List<String>> buildGlobalMapLocked() { 143 ServiceLoader<AppSearchDocumentClassMap> loader = ServiceLoader.load( 144 AppSearchDocumentClassMap.class, AppSearchDocumentClassMap.class.getClassLoader()); 145 Map<String, List<String>> result = new ArrayMap<>(); 146 for (AppSearchDocumentClassMap appSearchDocumentClassMap : loader) { 147 Map<String, List<String>> documentClassMap = appSearchDocumentClassMap.getMap(); 148 for (Map.Entry<String, List<String>> entry : documentClassMap.entrySet()) { 149 String schemaName = entry.getKey(); 150 // A single schema name can be mapped to more than one document classes because 151 // document classes can choose to have arbitrary schema names. The most common 152 // case is when there are multiple AppSearch packages that define the same schema 153 // name. It is necessary to keep track all of the mapped document classes to prevent 154 // from losing any information. 155 List<String> documentClassNames = result.get(schemaName); 156 if (documentClassNames == null) { 157 documentClassNames = new ArrayList<>(); 158 result.put(schemaName, documentClassNames); 159 } 160 documentClassNames.addAll(entry.getValue()); 161 } 162 } 163 164 for (String schemaName : result.keySet()) { 165 result.put(schemaName, 166 Collections.unmodifiableList(Objects.requireNonNull(result.get(schemaName)))); 167 } 168 return Collections.unmodifiableMap(result); 169 } 170 } 171