1package org.robolectric;
2
3import static com.google.common.collect.Lists.reverse;
4
5import com.google.common.annotations.VisibleForTesting;
6import java.io.IOException;
7import java.io.InputStream;
8import java.lang.reflect.Method;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.LinkedHashMap;
12import java.util.List;
13import java.util.Map;
14import java.util.Properties;
15import javax.annotation.Nonnull;
16import javax.annotation.Nullable;
17import org.robolectric.annotation.Config;
18import org.robolectric.util.Join;
19
20public class ConfigMerger {
21  private final Map<String, Config> packageConfigCache = new LinkedHashMap<String, Config>() {
22    @Override
23    protected boolean removeEldestEntry(Map.Entry eldest) {
24      return size() > 10;
25    }
26  };
27
28  /**
29   * Calculate the {@link Config} for the given test.
30   *
31   * @param testClass the class containing the test
32   * @param method the test method
33   * @param globalConfig global configuration values
34   * @return the effective configuration
35   * @since 3.2
36   */
37  public Config getConfig(Class<?> testClass, Method method, Config globalConfig) {
38    Config config = Config.Builder.defaults().build();
39    config = override(config, globalConfig);
40
41    for (String packageName : reverse(packageHierarchyOf(testClass))) {
42      Config packageConfig = cachedPackageConfig(packageName);
43      config = override(config, packageConfig);
44    }
45
46    for (Class clazz : reverse(parentClassesFor(testClass))) {
47      Config classConfig = (Config) clazz.getAnnotation(Config.class);
48      config = override(config, classConfig);
49    }
50
51    Config methodConfig = method.getAnnotation(Config.class);
52    config = override(config, methodConfig);
53
54    return config;
55  }
56
57  /**
58   * Generate {@link Config} for the specified package.
59   *
60   * More specific packages, test classes, and test method configurations
61   * will override values provided here.
62   *
63   * The default implementation uses properties provided by {@link #getConfigProperties(String)}.
64   *
65   * The returned object is likely to be reused for many tests.
66   *
67   * @param packageName the name of the package, or empty string ({@code ""}) for the top level package
68   * @return {@link Config} object for the specified package
69   * @since 3.2
70   */
71  @Nullable
72  private Config buildPackageConfig(String packageName) {
73    return Config.Implementation.fromProperties(getConfigProperties(packageName));
74  }
75
76  /**
77   * Return a {@link Properties} file for the given package name, or {@code null} if none is available.
78   *
79   * @since 3.2
80   */
81  protected Properties getConfigProperties(String packageName) {
82    List<String> packageParts = new ArrayList<>(Arrays.asList(packageName.split("\\.")));
83    packageParts.add(RobolectricTestRunner.CONFIG_PROPERTIES);
84    final String resourceName = Join.join("/", packageParts);
85    try (InputStream resourceAsStream = getResourceAsStream(resourceName)) {
86      if (resourceAsStream == null) return null;
87      Properties properties = new Properties();
88      properties.load(resourceAsStream);
89      return properties;
90    } catch (IOException e) {
91      throw new RuntimeException(e);
92    }
93  }
94
95  @Nonnull @VisibleForTesting
96  List<String> packageHierarchyOf(Class<?> javaClass) {
97    Package aPackage = javaClass.getPackage();
98    String testPackageName = aPackage == null ? "" : aPackage.getName();
99    List<String> packageHierarchy = new ArrayList<>();
100    while (!testPackageName.isEmpty()) {
101      packageHierarchy.add(testPackageName);
102      int lastDot = testPackageName.lastIndexOf('.');
103      testPackageName = lastDot > 1 ? testPackageName.substring(0, lastDot) : "";
104    }
105    packageHierarchy.add("");
106    return packageHierarchy;
107  }
108
109  @Nonnull
110  private List<Class> parentClassesFor(Class testClass) {
111    List<Class> testClassHierarchy = new ArrayList<>();
112    while (testClass != null && !testClass.equals(Object.class)) {
113      testClassHierarchy.add(testClass);
114      testClass = testClass.getSuperclass();
115    }
116    return testClassHierarchy;
117  }
118
119  private Config override(Config config, Config classConfig) {
120    return classConfig != null ? new Config.Builder(config).overlay(classConfig).build() : config;
121  }
122
123  @Nullable
124  private Config cachedPackageConfig(String packageName) {
125    synchronized (packageConfigCache) {
126      Config config = packageConfigCache.get(packageName);
127      if (config == null && !packageConfigCache.containsKey(packageName)) {
128        config = buildPackageConfig(packageName);
129        packageConfigCache.put(packageName, config);
130      }
131      return config;
132    }
133  }
134
135  // visible for testing
136  @SuppressWarnings("WeakerAccess")
137  InputStream getResourceAsStream(String resourceName) {
138    return getClass().getClassLoader().getResourceAsStream(resourceName);
139  }
140}
141