DocumentClassFactoryRegistry.java

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
// @exportToFramework:skipFile()
package androidx.appsearch.app;

import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.core.util.Preconditions;

import java.util.HashMap;
import java.util.Map;

/**
 * A registry which maintains instances of {@link DocumentClassFactory}.
 * @hide
 */
@AnyThread
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class DocumentClassFactoryRegistry {
    private static final String GEN_CLASS_PREFIX = "$$__AppSearch__";

    private static volatile DocumentClassFactoryRegistry sInstance = null;

    private final Map<Class<?>, DocumentClassFactory<?>> mFactories = new HashMap<>();

    private DocumentClassFactoryRegistry() {}

    /** Returns the singleton instance of {@link DocumentClassFactoryRegistry}. */
    @NonNull
    public static DocumentClassFactoryRegistry getInstance() {
        if (sInstance == null) {
            synchronized (DocumentClassFactoryRegistry.class) {
                if (sInstance == null) {
                    sInstance = new DocumentClassFactoryRegistry();
                }
            }
        }
        return sInstance;
    }

    /**
     * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
     * {@code T}.
     *
     * @throws AppSearchException if no factory for this document class could be found on the
     * classpath
     */
    @NonNull
    @SuppressWarnings("unchecked")
    public <T> DocumentClassFactory<T> getOrCreateFactory(@NonNull Class<T> documentClass)
            throws AppSearchException {
        Preconditions.checkNotNull(documentClass);
        DocumentClassFactory<?> factory;
        synchronized (this) {
            factory = mFactories.get(documentClass);
        }
        if (factory == null) {
            factory = loadFactoryByReflection(documentClass);
            synchronized (this) {
                DocumentClassFactory<?> racingFactory = mFactories.get(documentClass);
                if (racingFactory == null) {
                    mFactories.put(documentClass, factory);
                } else {
                    // Another thread beat us to it
                    factory = racingFactory;
                }
            }
        }
        return (DocumentClassFactory<T>) factory;
    }

    /**
     * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
     * {@code T}.
     *
     * @throws AppSearchException if no factory for this document class could be found on the
     * classpath
     */
    @NonNull
    @SuppressWarnings("unchecked")
    public <T> DocumentClassFactory<T> getOrCreateFactory(@NonNull T documentClass)
            throws AppSearchException {
        Preconditions.checkNotNull(documentClass);
        Class<?> clazz = documentClass.getClass();
        DocumentClassFactory<?> factory = getOrCreateFactory(clazz);
        return (DocumentClassFactory<T>) factory;
    }

    private DocumentClassFactory<?> loadFactoryByReflection(@NonNull Class<?> documentClass)
            throws AppSearchException {
        Package pkg = documentClass.getPackage();
        String simpleName = documentClass.getCanonicalName();
        if (simpleName == null) {
            throw new AppSearchException(
                    AppSearchResult.RESULT_INTERNAL_ERROR,
                    "Failed to find simple name for document class \"" + documentClass
                            + "\". Perhaps it is anonymous?");
        }

        // Creates factory class name under the package.
        // For a class Foo annotated with @Document, we will generated a
        // $$__AppSearch__Foo.class under the package.
        // For an inner class Foo.Bar annotated with @Document, we will generated a
        // $$__AppSearch__Foo$$__Bar.class under the package.
        String packageName = "";
        if (pkg != null) {
            packageName = pkg.getName() + ".";
            simpleName = simpleName.substring(packageName.length()).replace(".", "$$__");
        }
        String factoryClassName = packageName + GEN_CLASS_PREFIX + simpleName;

        Class<?> factoryClass;
        try {
            factoryClass = Class.forName(factoryClassName);
        } catch (ClassNotFoundException e) {
            throw new AppSearchException(
                    AppSearchResult.RESULT_INTERNAL_ERROR,
                    "Failed to find document class converter \"" + factoryClassName
                            + "\". Perhaps the annotation processor was not run or the class was "
                            + "proguarded out?",
                    e);
        }
        Object instance;
        try {
            instance = factoryClass.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new AppSearchException(
                    AppSearchResult.RESULT_INTERNAL_ERROR,
                    "Failed to construct document class converter \"" + factoryClassName + "\"",
                    e);
        }
        return (DocumentClassFactory<?>) instance;
    }
}