1 /*
2  * Copyright 2020 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 androidx.annotation.AnyThread;
20 import androidx.annotation.RestrictTo;
21 import androidx.appsearch.exceptions.AppSearchException;
22 import androidx.core.util.Preconditions;
23 
24 import org.jspecify.annotations.NonNull;
25 
26 import java.util.HashMap;
27 import java.util.Map;
28 
29 /**
30  * A registry which maintains instances of {@link DocumentClassFactory}.
31  * @exportToFramework:hide
32  */
33 @AnyThread
34 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
35 public final class DocumentClassFactoryRegistry {
36     private static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
37 
38     private static volatile DocumentClassFactoryRegistry sInstance = null;
39 
40     private final Map<Class<?>, DocumentClassFactory<?>> mFactories = new HashMap<>();
41 
DocumentClassFactoryRegistry()42     private DocumentClassFactoryRegistry() {}
43 
44     /** Returns the singleton instance of {@link DocumentClassFactoryRegistry}. */
getInstance()45     public static @NonNull DocumentClassFactoryRegistry getInstance() {
46         if (sInstance == null) {
47             synchronized (DocumentClassFactoryRegistry.class) {
48                 if (sInstance == null) {
49                     sInstance = new DocumentClassFactoryRegistry();
50                 }
51             }
52         }
53         return sInstance;
54     }
55 
56     /**
57      * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
58      * {@code T}.
59      *
60      * @throws AppSearchException if no factory for this document class could be found on the
61      * classpath
62      */
63     @SuppressWarnings("unchecked")
getOrCreateFactory(@onNull Class<T> documentClass)64     public <T> @NonNull DocumentClassFactory<T> getOrCreateFactory(@NonNull Class<T> documentClass)
65             throws AppSearchException {
66         Preconditions.checkNotNull(documentClass);
67         DocumentClassFactory<?> factory;
68         synchronized (this) {
69             factory = mFactories.get(documentClass);
70         }
71         if (factory == null) {
72             factory = loadFactoryByReflection(documentClass);
73             synchronized (this) {
74                 DocumentClassFactory<?> racingFactory = mFactories.get(documentClass);
75                 if (racingFactory == null) {
76                     mFactories.put(documentClass, factory);
77                 } else {
78                     // Another thread beat us to it
79                     factory = racingFactory;
80                 }
81             }
82         }
83         return (DocumentClassFactory<T>) factory;
84     }
85 
86     /**
87      * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
88      * {@code T}.
89      *
90      * @throws AppSearchException if no factory for this document class could be found on the
91      * classpath
92      */
93     @SuppressWarnings("unchecked")
getOrCreateFactory(@onNull T documentClass)94     public <T> @NonNull DocumentClassFactory<T> getOrCreateFactory(@NonNull T documentClass)
95             throws AppSearchException {
96         Preconditions.checkNotNull(documentClass);
97         Class<?> clazz = documentClass.getClass();
98         DocumentClassFactory<?> factory = getOrCreateFactory(clazz);
99         return (DocumentClassFactory<T>) factory;
100     }
101 
loadFactoryByReflection(@onNull Class<?> documentClass)102     private DocumentClassFactory<?> loadFactoryByReflection(@NonNull Class<?> documentClass)
103             throws AppSearchException {
104         Package pkg = documentClass.getPackage();
105         String simpleName = documentClass.getCanonicalName();
106         if (simpleName == null) {
107             throw new AppSearchException(
108                     AppSearchResult.RESULT_INTERNAL_ERROR,
109                     "Failed to find simple name for document class \"" + documentClass
110                             + "\". Perhaps it is anonymous?");
111         }
112 
113         // Creates factory class name under the package.
114         // For a class Foo annotated with @Document, we will generated a
115         // $$__AppSearch__Foo.class under the package.
116         // For an inner class Foo.Bar annotated with @Document, we will generated a
117         // $$__AppSearch__Foo$$__Bar.class under the package.
118         String packageName = "";
119         if (pkg != null) {
120             packageName = pkg.getName() + ".";
121             simpleName = simpleName.substring(packageName.length()).replace(".", "$$__");
122         }
123         String factoryClassName = packageName + GEN_CLASS_PREFIX + simpleName;
124 
125         Class<?> factoryClass;
126         try {
127             factoryClass = Class.forName(factoryClassName);
128         } catch (ClassNotFoundException e) {
129             // If the current class or interface has only one parent interface/class, then try to
130             // look at the unique parent.
131             Class<?> superClass = documentClass.getSuperclass();
132             Class<?>[] superInterfaces = documentClass.getInterfaces();
133             if (superClass == Object.class) {
134                 superClass = null;
135             }
136             int numParent = superInterfaces.length;
137             if (superClass != null) {
138                 numParent += 1;
139             }
140 
141             if (numParent == 1) {
142                 if (superClass != null) {
143                     return loadFactoryByReflection(superClass);
144                 } else {
145                     return loadFactoryByReflection(superInterfaces[0]);
146                 }
147             }
148 
149             String errorMessage = "Failed to find document class converter \"" + factoryClassName
150                     + "\". Perhaps the annotation processor was not run or the class was "
151                     + "proguarded out?";
152             if (numParent > 1) {
153                 errorMessage += " Or, this class may not have been annotated with @Document, and "
154                         + "there is an ambiguity to determine a unique @Document annotated parent "
155                         + "class/interface.";
156             }
157 
158             throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, errorMessage, e);
159         }
160         Object instance;
161         try {
162             instance = factoryClass.getDeclaredConstructor().newInstance();
163         } catch (Exception e) {
164             throw new AppSearchException(
165                     AppSearchResult.RESULT_INTERNAL_ERROR,
166                     "Failed to construct document class converter \"" + factoryClassName + "\"",
167                     e);
168         }
169         return (DocumentClassFactory<?>) instance;
170     }
171 }
172