Home | History | Annotate | Download | only in bytecode
      1 package org.robolectric.internal.bytecode;
      2 
      3 import com.google.common.collect.ImmutableList;
      4 import com.google.common.collect.ImmutableMap;
      5 import com.google.common.collect.ImmutableSet;
      6 import com.google.common.collect.Sets;
      7 import java.util.Collection;
      8 import java.util.Collections;
      9 import java.util.HashMap;
     10 import java.util.HashSet;
     11 import java.util.List;
     12 import java.util.Map;
     13 import java.util.Set;
     14 import org.robolectric.annotation.internal.DoNotInstrument;
     15 import org.robolectric.annotation.internal.Instrument;
     16 import org.robolectric.shadow.api.Shadow;
     17 
     18 /**
     19  * Configuration rules for {@link SandboxClassLoader}.
     20  */
     21 public class InstrumentationConfiguration {
     22 
     23   public static Builder newBuilder() {
     24     return new Builder();
     25   }
     26 
     27   static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet(
     28       RobolectricInternals.class.getName(),
     29       InvokeDynamicSupport.class.getName(),
     30       Shadow.class.getName(),
     31 
     32       // these classes are deprecated and will be removed soon:
     33       "org.robolectric.util.FragmentTestUtil",
     34       "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity"
     35   );
     36 
     37   static final Set<String> RESOURCES_TO_ALWAYS_ACQUIRE = Sets.newHashSet("build.prop");
     38 
     39   private final List<String> instrumentedPackages;
     40   private final Set<String> instrumentedClasses;
     41   private final Set<String> classesToNotInstrument;
     42   private final Map<String, String> classNameTranslations;
     43   private final Set<MethodRef> interceptedMethods;
     44   private final Set<String> classesToNotAcquire;
     45   private final Set<String> packagesToNotAcquire;
     46   private final Set<String> packagesToNotInstrument;
     47   private int cachedHashCode;
     48 
     49   private InstrumentationConfiguration(
     50       Map<String, String> classNameTranslations,
     51       Collection<MethodRef> interceptedMethods,
     52       Collection<String> instrumentedPackages,
     53       Collection<String> instrumentedClasses,
     54       Collection<String> classesToNotAcquire,
     55       Collection<String> packagesToNotAquire,
     56       Collection<String> classesToNotInstrument,
     57       Collection<String> packagesToNotInstrument) {
     58     this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations);
     59     this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods);
     60     this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages);
     61     this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses);
     62     this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire);
     63     this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire);
     64     this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument);
     65     this.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument);
     66     this.cachedHashCode = 0;
     67   }
     68 
     69   /**
     70    * Determine if {@link SandboxClassLoader} should instrument a given class.
     71    *
     72    * @param   classInfo The class to check.
     73    * @return  True if the class should be instrumented.
     74    */
     75   public boolean shouldInstrument(ClassInfo classInfo) {
     76     return !(classInfo.isInterface()
     77             || classInfo.isAnnotation()
     78             || classInfo.hasAnnotation(DoNotInstrument.class))
     79         && (isInInstrumentedPackage(classInfo)
     80             || instrumentedClasses.contains(classInfo.getName())
     81             || classInfo.hasAnnotation(Instrument.class))
     82         && !(classesToNotInstrument.contains(classInfo.getName()))
     83         && !(isInPackagesToNotInstrument(classInfo));
     84   }
     85 
     86   /**
     87    * Determine if {@link SandboxClassLoader} should load a given class.
     88    *
     89    * @param   name The fully-qualified class name.
     90    * @return  True if the class should be loaded.
     91    */
     92   public boolean shouldAcquire(String name) {
     93     if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) {
     94       return true;
     95     }
     96 
     97     if (name.equals("java.util.jar.StrictJarFile")) {
     98       return true;
     99     }
    100 
    101     // android.R and com.android.internal.R classes must be loaded from the framework jar
    102     if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) {
    103       return true;
    104     }
    105 
    106     // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864
    107     if (name.equals("javax.net.ssl.DistinguishedNameParser")
    108         || name.equals("javax.microedition.khronos.opengles.GL")) {
    109       return true;
    110     }
    111 
    112     for (String packageName : packagesToNotAcquire) {
    113       if (name.startsWith(packageName)) return false;
    114     }
    115 
    116     // R classes must be loaded from system CP
    117     boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$");
    118     return !isRClass && !classesToNotAcquire.contains(name);
    119   }
    120 
    121   /**
    122    * Determine if {@link SandboxClassLoader} should load a given resource.
    123    *
    124    * @param name The fully-qualified resource name.
    125    * @return True if the resource should be loaded.
    126    */
    127   public boolean shouldAcquireResource(String name) {
    128     return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name);
    129   }
    130 
    131   public Set<MethodRef> methodsToIntercept() {
    132     return Collections.unmodifiableSet(interceptedMethods);
    133   }
    134 
    135   /**
    136    * Map from a requested class to an alternate stand-in, or not.
    137    *
    138    * @return Mapping of class name translations.
    139    */
    140   public Map<String, String> classNameTranslations() {
    141     return Collections.unmodifiableMap(classNameTranslations);
    142   }
    143 
    144   public boolean containsStubs(ClassInfo classInfo) {
    145     return classInfo.getName().startsWith("com.google.android.maps.");
    146   }
    147 
    148   private boolean isInInstrumentedPackage(ClassInfo classInfo) {
    149     final String className = classInfo.getName();
    150     for (String instrumentedPackage : instrumentedPackages) {
    151       if (className.startsWith(instrumentedPackage)) {
    152         return true;
    153       }
    154     }
    155     return false;
    156   }
    157 
    158   private boolean isInPackagesToNotInstrument(ClassInfo classInfo) {
    159     final String className = classInfo.getName();
    160     for (String notInstrumentedPackage : packagesToNotInstrument) {
    161       if (className.startsWith(notInstrumentedPackage)) {
    162         return true;
    163       }
    164     }
    165     return false;
    166   }
    167 
    168   @Override
    169   public boolean equals(Object o) {
    170     if (this == o) return true;
    171     if (o == null || getClass() != o.getClass()) return false;
    172 
    173     InstrumentationConfiguration that = (InstrumentationConfiguration) o;
    174 
    175     if (!classNameTranslations.equals(that.classNameTranslations)) return false;
    176     if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false;
    177     if (!instrumentedPackages.equals(that.instrumentedPackages)) return false;
    178     if (!instrumentedClasses.equals(that.instrumentedClasses)) return false;
    179     if (!interceptedMethods.equals(that.interceptedMethods)) return false;
    180 
    181 
    182     return true;
    183   }
    184 
    185   @Override
    186   public int hashCode() {
    187     if (cachedHashCode != 0) {
    188       return cachedHashCode;
    189     }
    190 
    191     int result = instrumentedPackages.hashCode();
    192     result = 31 * result + instrumentedClasses.hashCode();
    193     result = 31 * result + classNameTranslations.hashCode();
    194     result = 31 * result + interceptedMethods.hashCode();
    195     result = 31 * result + classesToNotAcquire.hashCode();
    196     cachedHashCode = result;
    197     return result;
    198   }
    199 
    200   public static final class Builder {
    201     private final Collection<String> instrumentedPackages = new HashSet<>();
    202     private final Collection<MethodRef> interceptedMethods = new HashSet<>();
    203     private final Map<String, String> classNameTranslations = new HashMap<>();
    204     private final Collection<String> classesToNotAcquire = new HashSet<>();
    205     private final Collection<String> packagesToNotAcquire = new HashSet<>();
    206     private final Collection<String> instrumentedClasses = new HashSet<>();
    207     private final Collection<String> classesToNotInstrument = new HashSet<>();
    208     private final Collection<String> packagesToNotInstrument = new HashSet<>();
    209 
    210     public Builder() {
    211     }
    212 
    213     public Builder(InstrumentationConfiguration classLoaderConfig) {
    214       instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages);
    215       interceptedMethods.addAll(classLoaderConfig.interceptedMethods);
    216       classNameTranslations.putAll(classLoaderConfig.classNameTranslations);
    217       classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire);
    218       packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire);
    219       instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses);
    220       classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument);
    221       packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument);
    222     }
    223 
    224     public Builder doNotAcquireClass(Class<?> clazz) {
    225       doNotAcquireClass(clazz.getName());
    226       return this;
    227     }
    228 
    229     public Builder doNotAcquireClass(String className) {
    230       this.classesToNotAcquire.add(className);
    231       return this;
    232     }
    233 
    234     public Builder doNotAcquirePackage(String packageName) {
    235       this.packagesToNotAcquire.add(packageName);
    236       return this;
    237     }
    238 
    239     public Builder addClassNameTranslation(String fromName, String toName) {
    240       classNameTranslations.put(fromName, toName);
    241       return this;
    242     }
    243 
    244     public Builder addInterceptedMethod(MethodRef methodReference) {
    245       interceptedMethods.add(methodReference);
    246       return this;
    247     }
    248 
    249     public Builder addInstrumentedClass(String name) {
    250       instrumentedClasses.add(name);
    251       return this;
    252     }
    253 
    254     public Builder addInstrumentedPackage(String packageName) {
    255       instrumentedPackages.add(packageName);
    256       return this;
    257     }
    258 
    259     public Builder doNotInstrumentClass(String className) {
    260       this.classesToNotInstrument.add(className);
    261       return this;
    262     }
    263 
    264     public Builder doNotInstrumentPackage(String packageName) {
    265       this.packagesToNotInstrument.add(packageName);
    266       return this;
    267     }
    268 
    269     public InstrumentationConfiguration build() {
    270       return new InstrumentationConfiguration(
    271           classNameTranslations,
    272           interceptedMethods,
    273           instrumentedPackages,
    274           instrumentedClasses,
    275           classesToNotAcquire,
    276           packagesToNotAcquire,
    277           classesToNotInstrument,
    278           packagesToNotInstrument);
    279     }
    280   }
    281 }
    282