com.airbnb.deeplinkdispatch.DeepLinkProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.airbnb.deeplinkdispatch.DeepLinkProcessor.java

Source

/*
 * Copyright (C) 2015 Airbnb, Inc.
 *
 * 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.
 */
package com.airbnb.deeplinkdispatch;

import com.google.auto.common.AnnotationMirrors;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.service.AutoService;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Sets;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;

import static com.airbnb.deeplinkdispatch.MoreAnnotationMirrors.asAnnotationValues;
import static com.airbnb.deeplinkdispatch.MoreAnnotationMirrors.getTypeValue;
import static com.airbnb.deeplinkdispatch.Utils.decapitalize;
import static com.google.auto.common.MoreElements.getAnnotationMirror;

@AutoService(Processor.class)
public class DeepLinkProcessor extends AbstractProcessor {
    private static final ClassName ANDROID_BUNDLE = ClassName.get("android.os", "Bundle");
    private static final String DLD_PACKAGE_NAME = "com.airbnb.deeplinkdispatch";
    private static final ClassName ANDROID_INTENT = ClassName.get("android.content", "Intent");
    private static final ClassName ANDROID_CONTEXT = ClassName.get("android.content", "Context");
    private static final ClassName ANDROID_ACTIVITY = ClassName.get("android.app", "Activity");
    private static final ClassName TASK_STACK_BUILDER = ClassName.get("android.support.v4.app", "TaskStackBuilder");
    private static final ClassName ANDROID_URI = ClassName.get("android.net", "Uri");
    private static final ClassName CLASS_DLD_ENTRY = ClassName.get(DeepLinkEntry.class);
    private static final ClassName CLASS_DLD_URI = ClassName.get(DeepLinkUri.class);
    private static final ClassName CLASS_ARRAYS = ClassName.get(Arrays.class);
    private static final ClassName CLASS_COLLECTIONS = ClassName.get(Collections.class);
    private static final Class<DeepLink> DEEP_LINK_CLASS = DeepLink.class;
    private static final Class<DeepLinkSpec> DEEP_LINK_SPEC_CLASS = DeepLinkSpec.class;

    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Sets.newHashSet("*");
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<Element> customAnnotations = new HashSet<>();
        for (Element annotation : annotations) {
            if (annotation.getAnnotation(DEEP_LINK_SPEC_CLASS) != null) {
                customAnnotations.add(annotation);
            }
        }

        Map<Element, String[]> prefixes = new HashMap<>();
        Set<Element> customAnnotatedElements = new HashSet<>();
        for (Element customAnnotation : customAnnotations) {
            ElementKind kind = customAnnotation.getKind();
            if (kind != ElementKind.ANNOTATION_TYPE) {
                error(customAnnotation, "Only annotation types can be annotated with @%s",
                        DEEP_LINK_SPEC_CLASS.getSimpleName());
            }
            String[] prefix = customAnnotation.getAnnotation(DEEP_LINK_SPEC_CLASS).prefix();
            if (Utils.hasEmptyOrNullString(prefix)) {
                error(customAnnotation, "Prefix property cannot have null or empty strings");
            }
            if (prefix.length == 0) {
                error(customAnnotation, "Prefix property cannot be empty");
            }
            prefixes.put(customAnnotation, prefix);
            for (Element customAnnotatedElement : roundEnv
                    .getElementsAnnotatedWith(MoreElements.asType(customAnnotation))) {
                customAnnotatedElements.add(customAnnotatedElement);
            }
        }

        Set<Element> elementsToProcess = new HashSet<>(customAnnotatedElements);
        elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(DEEP_LINK_CLASS));

        List<DeepLinkAnnotatedElement> deepLinkElements = new ArrayList<>();
        for (Element element : elementsToProcess) {
            ElementKind kind = element.getKind();
            if (kind != ElementKind.METHOD && kind != ElementKind.CLASS) {
                error(element, "Only classes and methods can be annotated with @%s",
                        DEEP_LINK_CLASS.getSimpleName());
            }

            if (kind == ElementKind.METHOD) {
                Set<Modifier> methodModifiers = element.getModifiers();
                if (!methodModifiers.contains(Modifier.STATIC)) {
                    error(element, "Only static methods can be annotated with @%s",
                            DEEP_LINK_CLASS.getSimpleName());
                }
            }

            DeepLink deepLinkAnnotation = element.getAnnotation(DEEP_LINK_CLASS);
            List<String> deepLinks = new ArrayList<>();
            if (deepLinkAnnotation != null) {
                deepLinks.addAll(Arrays.asList(deepLinkAnnotation.value()));
            }
            if (customAnnotatedElements.contains(element)) {
                deepLinks.addAll(enumerateCustomDeepLinks(element, prefixes));
            }
            DeepLinkEntry.Type type = kind == ElementKind.CLASS ? DeepLinkEntry.Type.CLASS
                    : DeepLinkEntry.Type.METHOD;
            for (String deepLink : deepLinks) {
                try {
                    deepLinkElements.add(new DeepLinkAnnotatedElement(deepLink, element, type));
                } catch (MalformedURLException e) {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Malformed Deep Link URL " + deepLink);
                }
            }
        }
        Set<? extends Element> deepLinkHandlerElements = roundEnv.getElementsAnnotatedWith(DeepLinkHandler.class);
        for (Element deepLinkHandlerElement : deepLinkHandlerElements) {
            Optional<AnnotationMirror> annotationMirror = getAnnotationMirror(deepLinkHandlerElement,
                    DeepLinkHandler.class);
            if (annotationMirror.isPresent()) {
                Iterable<TypeMirror> klasses = getTypeValue(annotationMirror.get(), "value");
                List<TypeElement> typeElements = FluentIterable.from(klasses)
                        .transform(new Function<TypeMirror, TypeElement>() {
                            @Override
                            public TypeElement apply(TypeMirror klass) {
                                return MoreTypes.asTypeElement(klass);
                            }
                        }).toList();
                String packageName = processingEnv.getElementUtils().getPackageOf(deepLinkHandlerElement)
                        .getQualifiedName().toString();
                try {
                    generateDeepLinkDelegate(packageName, typeElements);
                } catch (IOException e) {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Error creating file");
                } catch (RuntimeException e) {
                    messager.printMessage(Diagnostic.Kind.ERROR,
                            "Internal error during annotation processing: " + e.getClass().getSimpleName());
                }
            }
        }

        Set<? extends Element> deepLinkModuleElements = roundEnv.getElementsAnnotatedWith(DeepLinkModule.class);
        for (Element deepLinkModuleElement : deepLinkModuleElements) {
            String packageName = processingEnv.getElementUtils().getPackageOf(deepLinkModuleElement)
                    .getQualifiedName().toString();
            try {
                generateDeepLinkLoader(packageName, deepLinkModuleElement.getSimpleName().toString(),
                        deepLinkElements);
            } catch (IOException e) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Error creating file");
            } catch (RuntimeException e) {
                messager.printMessage(Diagnostic.Kind.ERROR,
                        "Internal error during annotation processing: " + e.getClass().getSimpleName());
            }
        }

        return false;
    }

    private static List<String> enumerateCustomDeepLinks(Element element, Map<Element, String[]> prefixesMap) {
        Set<? extends AnnotationMirror> annotationMirrors = AnnotationMirrors.getAnnotatedAnnotations(element,
                DEEP_LINK_SPEC_CLASS);
        final List<String> deepLinks = new ArrayList<>();
        for (AnnotationMirror customAnnotation : annotationMirrors) {
            List<? extends AnnotationValue> suffixes = asAnnotationValues(
                    AnnotationMirrors.getAnnotationValue(customAnnotation, "value"));
            String[] prefixes = prefixesMap.get(customAnnotation.getAnnotationType().asElement());
            for (String prefix : prefixes) {
                for (AnnotationValue suffix : suffixes) {
                    deepLinks.add(prefix + suffix.getValue());
                }
            }
        }
        return deepLinks;
    }

    private void error(Element e, String msg, Object... args) {
        messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
    }

    private void generateDeepLinkLoader(String packageName, String className,
            List<DeepLinkAnnotatedElement> elements) throws IOException {
        CodeBlock.Builder initializer = CodeBlock.builder()
                .add("$T.unmodifiableList($T.asList(\n", CLASS_COLLECTIONS, CLASS_ARRAYS).indent();
        int totalElements = elements.size();
        for (int i = 0; i < totalElements; i++) {
            DeepLinkAnnotatedElement element = elements.get(i);
            String type = "DeepLinkEntry.Type." + element.getAnnotationType().toString();
            ClassName activity = ClassName.get(element.getAnnotatedElement());
            Object method = element.getMethod() == null ? null : element.getMethod();
            String uri = element.getUri();
            initializer.add("new DeepLinkEntry($S, $L, $T.class, $S)$L\n", uri, type, activity, method,
                    (i < totalElements - 1) ? "," : "");
        }
        FieldSpec registry = FieldSpec
                .builder(ParameterizedTypeName.get(List.class, DeepLinkEntry.class), "REGISTRY", Modifier.PUBLIC,
                        Modifier.FINAL, Modifier.STATIC)
                .initializer(initializer.unindent().add("))").build()).build();

        MethodSpec parseMethod = MethodSpec.methodBuilder("parseUri").addModifiers(Modifier.PUBLIC)
                .addAnnotation(AnnotationSpec.builder(Override.class).build()).addParameter(String.class, "uri")
                .returns(DeepLinkEntry.class).beginControlFlow("for (DeepLinkEntry entry : REGISTRY)")
                .beginControlFlow("if (entry.matches(uri))").addStatement("return entry").endControlFlow()
                .endControlFlow().addStatement("return null").build();

        TypeSpec deepLinkLoader = TypeSpec.classBuilder(className + "Loader")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL).addSuperinterface(ClassName.get(Parser.class))
                .addField(registry).addMethod(parseMethod).build();

        JavaFile.builder(packageName, deepLinkLoader).build().writeTo(filer);
    }

    private static String moduleNameToLoaderName(TypeElement typeElement) {
        return typeElement.getSimpleName().toString() + "Loader";
    }

    private static ClassName moduleElementToLoaderClassName(TypeElement element) {
        return ClassName.get(getPackage(element).getQualifiedName().toString(),
                element.getSimpleName().toString() + "Loader");
    }

    private static PackageElement getPackage(Element type) {
        while (type.getKind() != ElementKind.PACKAGE) {
            type = type.getEnclosingElement();
        }
        return (PackageElement) type;
    }

    private void generateDeepLinkDelegate(String packageName, List<TypeElement> loaderClasses) throws IOException {
        MethodSpec notifyListenerMethod = MethodSpec.methodBuilder("notifyListener")
                .addModifiers(Modifier.PRIVATE, Modifier.STATIC).returns(void.class)
                .addParameter(ANDROID_CONTEXT, "context").addParameter(boolean.class, "isError")
                .addParameter(ClassName.get("android.net", "Uri"), "uri").addParameter(String.class, "errorMessage")
                .addStatement("$T intent = new Intent()", ANDROID_INTENT)
                .addStatement("intent.setAction($T.ACTION)", DeepLinkHandler.class)
                .addStatement("intent.putExtra($T.EXTRA_URI, uri != null ? uri.toString() : $S)",
                        DeepLinkHandler.class, "")
                .addStatement("intent.putExtra($T.EXTRA_SUCCESSFUL, !isError)", DeepLinkHandler.class)
                .beginControlFlow("if (isError)")
                .addStatement("intent.putExtra($T.EXTRA_ERROR_MESSAGE, errorMessage)", DeepLinkHandler.class)
                .endControlFlow().addStatement("$T.getInstance(context).sendBroadcast(intent)",
                        ClassName.get("android.support.v4.content", "LocalBroadcastManager"))
                .build();
        ClassName deepLinkResult = ClassName.get(DLD_PACKAGE_NAME, "DeepLinkResult");
        MethodSpec createResultAndNotifyMethod = MethodSpec.methodBuilder("createResultAndNotify")
                .addModifiers(Modifier.PRIVATE, Modifier.STATIC).returns(deepLinkResult)
                .addParameter(ANDROID_CONTEXT, "context")
                .addParameter(TypeName.BOOLEAN, "successful", Modifier.FINAL)
                .addParameter(ANDROID_URI, "uri", Modifier.FINAL)
                .addParameter(ClassName.get(String.class), "error", Modifier.FINAL)
                .addStatement("notifyListener(context, !successful, uri, error)")
                .addStatement("return new $T(successful, uri != null ? uri.toString() : null, error)",
                        deepLinkResult)
                .build();

        FieldSpec tag = FieldSpec.builder(String.class, "TAG", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
                .initializer("DeepLinkDelegate.class.getSimpleName()").build();

        FieldSpec loaders = FieldSpec.builder(
                ParameterizedTypeName.get(ClassName.get(List.class), WildcardTypeName.subtypeOf(Parser.class)),
                "loaders", Modifier.PRIVATE, Modifier.FINAL).build();

        CodeBlock.Builder loadersInitializer = CodeBlock.builder()
                .add("this.loaders = $T.asList(\n", ClassName.get(Arrays.class)).indent();
        int totalElements = loaderClasses.size();
        for (int i = 0; i < totalElements; i++) {
            loadersInitializer.add("$L$L", decapitalize(moduleNameToLoaderName(loaderClasses.get(i))),
                    i < totalElements - 1 ? "," : "");
        }
        MethodSpec constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameters(
                FluentIterable.from(loaderClasses).transform(new Function<TypeElement, ParameterSpec>() {
                    @Override
                    public ParameterSpec apply(TypeElement typeElement) {
                        return ParameterSpec.builder(moduleElementToLoaderClassName(typeElement),
                                decapitalize(moduleNameToLoaderName(typeElement))).build();
                    }
                }).toList()).addCode(loadersInitializer.unindent().add(");\n").build()).build();

        MethodSpec supportsUri = MethodSpec.methodBuilder("supportsUri").addModifiers(Modifier.PUBLIC)
                .returns(TypeName.BOOLEAN).addParameter(String.class, "uriString")
                .addStatement("return findEntry(uriString) != null").build();

        MethodSpec findEntry = MethodSpec.methodBuilder("findEntry").addModifiers(Modifier.PRIVATE)
                .returns(CLASS_DLD_ENTRY).addParameter(String.class, "uriString")
                .beginControlFlow("for (Parser loader : loaders)")
                .addStatement("$T entry = loader.parseUri(uriString)", CLASS_DLD_ENTRY)
                .beginControlFlow("if (entry != null)").addStatement("return entry").endControlFlow()
                .endControlFlow().addStatement("return null").build();

        MethodSpec dispatchFromMethod = MethodSpec.methodBuilder("dispatchFrom").addModifiers(Modifier.PUBLIC)
                .returns(deepLinkResult).addParameter(ANDROID_ACTIVITY, "activity")
                .beginControlFlow("if (activity == null)")
                .addStatement("throw new $T($S)", NullPointerException.class, "activity == null").endControlFlow()
                .addStatement("return dispatchFrom(activity, activity.getIntent())").build();

        MethodSpec dispatchFromMethodWithIntent = MethodSpec.methodBuilder("dispatchFrom")
                .addModifiers(Modifier.PUBLIC).returns(deepLinkResult).addParameter(ANDROID_ACTIVITY, "activity")
                .addParameter(ANDROID_INTENT, "sourceIntent").beginControlFlow("if (activity == null)")
                .addStatement("throw new $T($S)", NullPointerException.class, "activity == null").endControlFlow()
                .beginControlFlow("if (sourceIntent == null)")
                .addStatement("throw new $T($S)", NullPointerException.class, "sourceIntent == null")
                .endControlFlow().addStatement("$T uri = sourceIntent.getData()", ANDROID_URI)
                .beginControlFlow("if (uri == null)")
                .addStatement("return createResultAndNotify(activity, false, null, $S)",
                        "No Uri in given activity's intent.")
                .endControlFlow().addStatement("String uriString = uri.toString()")
                .addStatement("$T entry = findEntry(uriString)", CLASS_DLD_ENTRY)
                .beginControlFlow("if (entry != null)")
                .addStatement("$T deepLinkUri = DeepLinkUri.parse(uriString)", CLASS_DLD_URI)
                .addStatement("$T<String, String> parameterMap = entry.getParameters(uriString)", Map.class)
                .beginControlFlow("for (String queryParameter : deepLinkUri.queryParameterNames())")
                .beginControlFlow(
                        "for (String queryParameterValue : deepLinkUri.queryParameterValues(queryParameter))")
                .beginControlFlow("if (parameterMap.containsKey(queryParameter))")
                .addStatement("$T.w(TAG, \"Duplicate parameter name in path and query param: \" + queryParameter)",
                        ClassName.get("android.util", "Log"))
                .endControlFlow().addStatement("parameterMap.put(queryParameter, queryParameterValue)")
                .endControlFlow().endControlFlow()
                .addStatement("parameterMap.put($T.URI, uri.toString())",
                        ClassName.get(DLD_PACKAGE_NAME, "DeepLink"))
                .addStatement("$T parameters", ANDROID_BUNDLE)
                .beginControlFlow("if (sourceIntent.getExtras() != null)")
                .addStatement("parameters = new Bundle(sourceIntent.getExtras())").nextControlFlow("else")
                .addStatement("parameters = new Bundle()").endControlFlow()
                .beginControlFlow("for (Map.Entry<String, String> parameterEntry : parameterMap.entrySet())")
                .addStatement("parameters.putString(parameterEntry.getKey(), parameterEntry.getValue())")
                .endControlFlow().beginControlFlow("try").addStatement("Class<?> c = entry.getActivityClass()")
                .addStatement("$T newIntent", ANDROID_INTENT)
                .addStatement("$T taskStackBuilder = null", TASK_STACK_BUILDER)
                .beginControlFlow("if (entry.getType() == DeepLinkEntry.Type.CLASS)")
                .addStatement("newIntent = new Intent(activity, c)").nextControlFlow("else")
                .addStatement("$T method", Method.class).beginControlFlow("try")
                .addStatement("method = c.getMethod(entry.getMethod(), $T.class)", ANDROID_CONTEXT)
                .beginControlFlow("if (method.getReturnType().equals($T.class))", TASK_STACK_BUILDER)
                .addStatement("taskStackBuilder = (TaskStackBuilder) method.invoke(c, activity)")
                .beginControlFlow("if (taskStackBuilder.getIntentCount() == 0)")
                .addStatement("return createResultAndNotify(activity, false, uri, \"Could not deep "
                        + "link to method: \" + entry.getMethod() + \" intents length == 0\" )")
                .endControlFlow()
                .addStatement("newIntent = taskStackBuilder." + "editIntentAt(taskStackBuilder.getIntentCount()-1)")
                .nextControlFlow("else").addStatement("newIntent = (Intent) method.invoke(c, activity)")
                .endControlFlow().nextControlFlow("catch ($T exception)", NoSuchMethodException.class)
                .addStatement("method = c.getMethod(entry.getMethod(), $T.class, $T.class)", ANDROID_CONTEXT,
                        ANDROID_BUNDLE)
                .beginControlFlow("if (method.getReturnType().equals($T.class))", TASK_STACK_BUILDER)
                .addStatement("taskStackBuilder = " + "(TaskStackBuilder) method.invoke(c, activity, parameters)")
                .beginControlFlow("if (taskStackBuilder.getIntentCount() == 0)")
                .addStatement("return createResultAndNotify(activity, false, uri, \"Could not deep "
                        + "link to method: \" + entry.getMethod() + \" intents length == 0\" )")
                .endControlFlow()
                .addStatement("newIntent = taskStackBuilder." + "editIntentAt(taskStackBuilder.getIntentCount()-1)")
                .nextControlFlow("else").addStatement("newIntent = (Intent) method.invoke(c, activity, parameters)")
                .endControlFlow().endControlFlow().endControlFlow()
                .beginControlFlow("if (newIntent.getAction() == null)")
                .addStatement("newIntent.setAction(sourceIntent.getAction())").endControlFlow()
                .beginControlFlow("if (newIntent.getData() == null)")
                .addStatement("newIntent.setData(sourceIntent.getData())").endControlFlow()
                .addStatement("newIntent.putExtras(parameters)")
                .addStatement("newIntent.putExtra(DeepLink.IS_DEEP_LINK, true)")
                .addStatement("newIntent.putExtra(DeepLink.REFERRER_URI, uri)")
                .beginControlFlow("if (activity.getCallingActivity() != null)")
                .addStatement("newIntent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)").endControlFlow()
                .beginControlFlow("if (taskStackBuilder != null)")
                .addStatement("taskStackBuilder.startActivities()").nextControlFlow("else")
                .addStatement("activity.startActivity(newIntent)").endControlFlow()
                .addStatement("return createResultAndNotify(activity, true, uri, null)")
                .nextControlFlow("catch (NoSuchMethodException exception)")
                .addStatement("return createResultAndNotify(activity, false, uri, \"Deep link to "
                        + "non-existent method: \" + entry.getMethod())")
                .nextControlFlow("catch (IllegalAccessException exception)")
                .addStatement("return createResultAndNotify(activity, false, uri, \"Could not deep "
                        + "link to method: \" + entry.getMethod())")
                .nextControlFlow("catch ($T  exception)", InvocationTargetException.class)
                .addStatement("return createResultAndNotify(activity, false, uri, \"Could not deep "
                        + "link to method: \" + entry.getMethod())")
                .endControlFlow().nextControlFlow("else")
                .addStatement("return createResultAndNotify(activity, false, uri, "
                        + "\"No registered entity to handle deep link: \" + uri.toString())")
                .endControlFlow().build();

        TypeSpec deepLinkDelegate = TypeSpec.classBuilder("DeepLinkDelegate")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL).addField(tag).addField(loaders)
                .addMethod(constructor).addMethod(findEntry).addMethod(dispatchFromMethod)
                .addMethod(dispatchFromMethodWithIntent).addMethod(createResultAndNotifyMethod)
                .addMethod(notifyListenerMethod).addMethod(supportsUri).build();

        JavaFile.builder(packageName, deepLinkDelegate).build().writeTo(filer);
    }
}