package com.airbnb.epoxy; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import static com.airbnb.epoxy.Utils.buildEpoxyException; /** Manages configuration settings for different packages. */ class ConfigManager { static final String PROCESSOR_OPTION_VALIDATE_MODEL_USAGE = "validateEpoxyModelUsage"; static final String PROCESSOR_OPTION_REQUIRE_HASHCODE = "requireHashCodeInEpoxyModels"; static final String PROCESSOR_OPTION_REQUIRE_ABSTRACT_MODELS = "requireAbstractEpoxyModels"; static final String PROCESSOR_IMPLICITLY_ADD_AUTO_MODELS = "implicitlyAddAutoModels"; private static final PackageConfigSettings DEFAULT_PACKAGE_CONFIG_SETTINGS = PackageConfigSettings.forDefaults(); private final Map<String, PackageConfigSettings> configurationMap = new HashMap<>(); private final Elements elementUtils; private final boolean validateModelUsage; private final boolean globalRequireHashCode; private final boolean globalRequireAbstractModels; private final boolean globalImplicitlyAddAutoModels; ConfigManager(Map<String, String> options, Elements elementUtils) { this.elementUtils = elementUtils; validateModelUsage = getBooleanOption(options, PROCESSOR_OPTION_VALIDATE_MODEL_USAGE, true); globalRequireHashCode = getBooleanOption(options, PROCESSOR_OPTION_REQUIRE_HASHCODE, PackageEpoxyConfig.REQUIRE_HASHCODE_DEFAULT); globalRequireAbstractModels = getBooleanOption(options, PROCESSOR_OPTION_REQUIRE_ABSTRACT_MODELS, PackageEpoxyConfig.REQUIRE_ABSTRACT_MODELS_DEFAULT); globalImplicitlyAddAutoModels = getBooleanOption(options, PROCESSOR_IMPLICITLY_ADD_AUTO_MODELS, PackageEpoxyConfig.IMPLICITLY_ADD_AUTO_MODELS_DEFAULT); } private static boolean getBooleanOption(Map<String, String> options, String option, boolean defaultValue) { String value = options.get(option); if (value == null) { return defaultValue; } return Boolean.valueOf(value); } /** * If true, Epoxy models added to an EpoxyController will be * validated at run time to make sure they are properly used. * <p> * By default this is true, and it is highly recommended to enable it to prevent accidental misuse * of your models. However, you may want to disable this for production builds to avoid the * overhead of the runtime validation code. * <p> * Using a debug build flag is a great way to do this. */ List<Exception> processConfigurations(RoundEnvironment roundEnv) { Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(PackageEpoxyConfig.class); List<Exception> errors = new ArrayList<>(); for (Element element : annotatedElements) { String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString(); if (configurationMap.containsKey(packageName)) { errors.add(buildEpoxyException( "Only one Epoxy configuration annotation is allowed per package (%s)", packageName)); } PackageEpoxyConfig annotation = element.getAnnotation(PackageEpoxyConfig.class); configurationMap.put(packageName, PackageConfigSettings.create(annotation)); } return errors; } boolean requiresHashCode(AttributeInfo attributeInfo) { return globalRequireHashCode || getConfigurationForPackage(attributeInfo.getPackageName()).requireHashCode; } boolean requiresAbstractModels(TypeElement classElement) { return globalRequireAbstractModels || getConfigurationForElement(classElement).requireAbstractModels; } boolean implicitlyAddAutoModels(ControllerClassInfo controller) { return globalImplicitlyAddAutoModels || getConfigurationForElement(controller.controllerClassElement).implicitlyAddAutoModels; } boolean shouldValidateModelUsage() { return validateModelUsage; } private PackageConfigSettings getConfigurationForElement(Element element) { return getConfigurationForPackage(elementUtils.getPackageOf(element)); } private PackageConfigSettings getConfigurationForPackage(PackageElement packageElement) { String packageName = packageElement.getQualifiedName().toString(); return getConfigurationForPackage(packageName); } private PackageConfigSettings getConfigurationForPackage(String packageName) { if (configurationMap.containsKey(packageName)) { return configurationMap.get(packageName); } // If there isn't a configuration for that exact package then we look for configurations for // parent packages which include the target package. If multiple parent packages declare // configurations we take the configuration from the more nested parent. Entry<String, PackageConfigSettings> bestMatch = null; for (Entry<String, PackageConfigSettings> configEntry : configurationMap.entrySet()) { String entryPackage = configEntry.getKey(); if (!packageName.startsWith(entryPackage + ".")) { continue; } if (bestMatch == null || bestMatch.getKey().length() < entryPackage.length()) { bestMatch = configEntry; } } return bestMatch != null ? bestMatch.getValue() : DEFAULT_PACKAGE_CONFIG_SETTINGS; } }