aboutsummaryrefslogtreecommitdiffstats
path: root/guava-testlib/src/com/google/common/collect/testing/features/FeatureUtil.java
blob: 0351b1659e9de19a110014d6d581fbbec8c23857 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/*
 * Copyright (C) 2008 The Guava Authors
 *
 * 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.google.common.collect.testing.features;

import com.google.common.annotations.GwtCompatible;
import com.google.common.collect.testing.Helpers;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Utilities for collecting and validating tester requirements from annotations.
 *
 * <p>This class can be referenced in GWT tests.
 *
 * @author George van den Driessche
 */
@GwtCompatible
public class FeatureUtil {
  /**
   * A cache of annotated objects (typically a Class or Method) to its
   * set of annotations.
   */
  private static Map<AnnotatedElement, Annotation[]> annotationCache =
      new HashMap<AnnotatedElement, Annotation[]>();

  private static final Map<Class<?>, TesterRequirements>
      classTesterRequirementsCache =
          new HashMap<Class<?>, TesterRequirements>();

  /**
   * Given a set of features, add to it all the features directly or indirectly
   * implied by any of them, and return it.
   * @param features the set of features to expand
   * @return the same set of features, expanded with all implied features
   */
  public static Set<Feature<?>> addImpliedFeatures(Set<Feature<?>> features) {
    // The base case of the recursion is an empty set of features, which will
    // occur when the previous set contained only simple features.
    if (!features.isEmpty()) {
      features.addAll(impliedFeatures(features));
    }
    return features;
  }

  /**
   * Given a set of features, return a new set of all features directly or
   * indirectly implied by any of them.
   * @param features the set of features whose implications to find
   * @return the implied set of features
   */
  public static Set<Feature<?>> impliedFeatures(Set<Feature<?>> features) {
    Set<Feature<?>> implied = new LinkedHashSet<Feature<?>>();
    for (Feature<?> feature : features) {
      implied.addAll(feature.getImpliedFeatures());
    }
    addImpliedFeatures(implied);
    return implied;
  }

  /**
   * Get the full set of requirements for a tester class.
   * @param testerClass a tester class
   * @return all the constraints implicitly or explicitly required by the class
   * or any of its superclasses.
   * @throws ConflictingRequirementsException if the requirements are mutually
   * inconsistent.
   */
  public static TesterRequirements getTesterRequirements(Class<?> testerClass)
      throws ConflictingRequirementsException {
    synchronized (classTesterRequirementsCache) {
      TesterRequirements requirements =
          classTesterRequirementsCache.get(testerClass);
      if (requirements == null) {
        requirements = buildTesterRequirements(testerClass);
        classTesterRequirementsCache.put(testerClass, requirements);
      }
      return requirements;
    }
  }

  /**
   * Get the full set of requirements for a tester class.
   * @param testerMethod a test method of a tester class
   * @return all the constraints implicitly or explicitly required by the
   * method, its declaring class, or any of its superclasses.
   * @throws ConflictingRequirementsException if the requirements are
   * mutually inconsistent.
   */
  public static TesterRequirements getTesterRequirements(Method testerMethod)
      throws ConflictingRequirementsException {
    return buildTesterRequirements(testerMethod);
  }

  /**
   * Construct the full set of requirements for a tester class.
   * @param testerClass a tester class
   * @return all the constraints implicitly or explicitly required by the class
   * or any of its superclasses.
   * @throws ConflictingRequirementsException if the requirements are mutually
   * inconsistent.
   */
  static TesterRequirements buildTesterRequirements(Class<?> testerClass)
      throws ConflictingRequirementsException {
    final TesterRequirements declaredRequirements =
        buildDeclaredTesterRequirements(testerClass);
    Class<?> baseClass = testerClass.getSuperclass();
    if (baseClass == null) {
      return declaredRequirements;
    } else {
      final TesterRequirements clonedBaseRequirements =
          new TesterRequirements(getTesterRequirements(baseClass));
      return incorporateRequirements(
          clonedBaseRequirements, declaredRequirements, testerClass);
    }
  }

  /**
   * Construct the full set of requirements for a tester method.
   * @param testerMethod a test method of a tester class
   * @return all the constraints implicitly or explicitly required by the
   * method, its declaring class, or any of its superclasses.
   * @throws ConflictingRequirementsException if the requirements are mutually
   * inconsistent.
   */
  static TesterRequirements buildTesterRequirements(Method testerMethod)
      throws ConflictingRequirementsException {
    TesterRequirements clonedClassRequirements = new TesterRequirements(
        getTesterRequirements(testerMethod.getDeclaringClass()));
    TesterRequirements declaredRequirements =
        buildDeclaredTesterRequirements(testerMethod);
    return incorporateRequirements(
        clonedClassRequirements, declaredRequirements, testerMethod);
  }

  /**
   * Construct the set of requirements specified by annotations
   * directly on a tester class or method.
   * @param classOrMethod a tester class or a test method thereof
   * @return all the constraints implicitly or explicitly required by
   *         annotations on the class or method.
   * @throws ConflictingRequirementsException if the requirements are mutually
   *         inconsistent.
   */
  public static TesterRequirements buildDeclaredTesterRequirements(
      AnnotatedElement classOrMethod)
      throws ConflictingRequirementsException {
    TesterRequirements requirements = new TesterRequirements();

    Iterable<Annotation> testerAnnotations =
        getTesterAnnotations(classOrMethod);
    for (Annotation testerAnnotation : testerAnnotations) {
      TesterRequirements moreRequirements =
          buildTesterRequirements(testerAnnotation);
      incorporateRequirements(
          requirements, moreRequirements, testerAnnotation);
    }

    return requirements;
  }

  /**
   * Find all the tester annotations declared on a tester class or method.
   * @param classOrMethod a class or method whose tester annotations to find
   * @return an iterable sequence of tester annotations on the class
   */
  public static Iterable<Annotation> getTesterAnnotations(
      AnnotatedElement classOrMethod) {
    List<Annotation> result = new ArrayList<Annotation>();

    Annotation[] annotations;
    synchronized (annotationCache) {
      annotations = annotationCache.get(classOrMethod);
      if (annotations == null) {
        annotations = classOrMethod.getDeclaredAnnotations();
        annotationCache.put(classOrMethod, annotations);
      }
    }

    for (Annotation a : annotations) {
      Class<? extends Annotation> annotationClass = a.annotationType();
      if (annotationClass.isAnnotationPresent(TesterAnnotation.class)) {
        result.add(a);
      }
    }
    return result;
  }

  /**
   * Find all the constraints explicitly or implicitly specified by a single
   * tester annotation.
   * @param testerAnnotation a tester annotation
   * @return the requirements specified by the annotation
   * @throws ConflictingRequirementsException if the requirements are mutually
   *         inconsistent.
   */
  private static TesterRequirements buildTesterRequirements(
      Annotation testerAnnotation)
      throws ConflictingRequirementsException {
    Class<? extends Annotation> annotationClass = testerAnnotation.getClass();
    final Feature<?>[] presentFeatures;
    final Feature<?>[] absentFeatures;
    try {
      presentFeatures = (Feature[]) annotationClass.getMethod("value")
          .invoke(testerAnnotation);
      absentFeatures = (Feature[]) annotationClass.getMethod("absent")
          .invoke(testerAnnotation);
    } catch (Exception e) {
      throw new IllegalArgumentException(
          "Error extracting features from tester annotation.", e);
    }
    Set<Feature<?>> allPresentFeatures =
        addImpliedFeatures(Helpers.<Feature<?>>copyToSet(presentFeatures));
    Set<Feature<?>> allAbsentFeatures =
        addImpliedFeatures(Helpers.<Feature<?>>copyToSet(absentFeatures));
    Set<Feature<?>> conflictingFeatures =
        intersection(allPresentFeatures, allAbsentFeatures);
    if (!conflictingFeatures.isEmpty()) {
      throw new ConflictingRequirementsException("Annotation explicitly or " +
          "implicitly requires one or more features to be both present " +
          "and absent.",
          conflictingFeatures, testerAnnotation);
    }
    return new TesterRequirements(allPresentFeatures, allAbsentFeatures);
  }

  /**
   * Incorporate additional requirements into an existing requirements object.
   * @param requirements the existing requirements object
   * @param moreRequirements more requirements to incorporate
   * @param source the source of the additional requirements
   *        (used only for error reporting)
   * @return the existing requirements object, modified to include the
   *         additional requirements
   * @throws ConflictingRequirementsException if the additional requirements
   *         are inconsistent with the existing requirements
   */
  private static TesterRequirements incorporateRequirements(
      TesterRequirements requirements, TesterRequirements moreRequirements,
      Object source) throws ConflictingRequirementsException {
    Set<Feature<?>> presentFeatures = requirements.getPresentFeatures();
    Set<Feature<?>> absentFeatures = requirements.getAbsentFeatures();
    Set<Feature<?>> morePresentFeatures = moreRequirements.getPresentFeatures();
    Set<Feature<?>> moreAbsentFeatures = moreRequirements.getAbsentFeatures();
    checkConflict(
        "absent", absentFeatures,
        "present", morePresentFeatures, source);
    checkConflict(
        "present", presentFeatures,
        "absent", moreAbsentFeatures, source);
    presentFeatures.addAll(morePresentFeatures);
    absentFeatures.addAll(moreAbsentFeatures);
    return requirements;
  }

  // Used by incorporateRequirements() only
  private static void checkConflict(
      String earlierRequirement, Set<Feature<?>> earlierFeatures,
      String newRequirement, Set<Feature<?>> newFeatures,
      Object source) throws ConflictingRequirementsException {
    Set<Feature<?>> conflictingFeatures;
    conflictingFeatures = intersection(newFeatures, earlierFeatures);
    if (!conflictingFeatures.isEmpty()) {
      throw new ConflictingRequirementsException(String.format(
          "Annotation requires to be %s features that earlier " +
          "annotations required to be %s.",
              newRequirement, earlierRequirement),
          conflictingFeatures, source);
    }
  }

  /**
   * Construct a new {@link java.util.Set} that is the intersection
   * of the given sets.
   */
  // Calls generic varargs method.
  @SuppressWarnings("unchecked")
  public static <T> Set<T> intersection(
      Set<? extends T> set1, Set<? extends T> set2) {
    return intersection(new Set[] {set1, set2});
  }

  /**
   * Construct a new {@link java.util.Set} that is the intersection
   * of all the given sets.
   * @param sets the sets to intersect
   * @return the intersection of the sets
   * @throws java.lang.IllegalArgumentException if there are no sets to
   *         intersect
   */
  public static <T> Set<T> intersection(Set<? extends T> ... sets) {
    if (sets.length == 0) {
      throw new IllegalArgumentException(
          "Can't intersect no sets; would have to return the universe.");
    }
    Set<T> results = Helpers.copyToSet(sets[0]);
    for (int i = 1; i < sets.length; i++) {
      Set<? extends T> set = sets[i];
      results.retainAll(set);
    }
    return results;
  }
}