View Javadoc
1   package org.wikimedia.search.extra.superdetectnoop;
2   
3   import java.util.List;
4   import java.util.Locale;
5   import java.util.Objects;
6   
7   import javax.annotation.Nonnull;
8   import javax.annotation.Nullable;
9   
10  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
11  
12  /**
13   * Detects if two values are different enough to be changed.
14   *
15   * @param <T> type of the thin being checked
16   */
17  public interface ChangeHandler<T> {
18      /**
19       * Handle a proposed change.
20       */
21      Result handle(@Nullable T oldValue, @Nullable T newValue);
22  
23      /**
24       * Handler that must be wrapped with the NullSafe handler.
25       * @param <T> type on which the implementation operates
26       */
27      interface NonnullChangeHandler<T> {
28          Result handle(@Nonnull T oldValue, @Nonnull T newValue);
29      }
30  
31      /**
32       * Builds CloseEnoughDetectors from the string description sent in the
33       * script parameters. Returning null from build just means that this recognizer
34       * doesn't recognize that parameter.
35       */
36      interface Recognizer {
37          @Nullable
38          ChangeHandler<Object> build(String description);
39      }
40  
41      /**
42       * The result of the close enough check.
43       */
44      interface Result {
45          /**
46           * Were the two values close enough? Returning true will cause the
47           * values to be unchanged.
48           */
49          boolean isCloseEnough();
50  
51          /**
52           * If the two values weren't close enough what should we use as the new
53           * value? If the two values were close enough this is undefined. If this
54           * returns null then the value should be removed from the source.
55           */
56          @Nullable
57          Object newValue();
58  
59          /**
60           * Should the entire document update be noop'd?
61           */
62          boolean isDocumentNooped();
63      }
64  
65      /**
66       * Wraps another detector and only delegates to it if both values aren't
67       * null. If both values are null returns true, if only one is null then
68       * returns false.
69       *
70       * @param <T> type on which the wrapped detector operates
71       */
72      class NullSafe<T> implements ChangeHandler<T> {
73          private final NonnullChangeHandler<T> delegate;
74  
75          public NullSafe(NonnullChangeHandler<T> delegate) {
76              this.delegate = delegate;
77          }
78  
79          @Override
80          public Result handle(@Nullable T oldValue, @Nullable T newValue) {
81              if (oldValue == null) {
82                  return Changed.forBoolean(newValue == null, newValue);
83              }
84              if (newValue == null) {
85                  return new Changed(null);
86              }
87              return delegate.handle(oldValue, newValue);
88          }
89      }
90  
91      /**
92       * Objects are only close enough if they are {@link Object#equals(Object)}
93       * to each other.
94       */
95      final class Equal implements ChangeHandler<Object> {
96          public static final ChangeHandler<Object> INSTANCE = new Equal();
97  
98          public static class Recognizer implements ChangeHandler.Recognizer {
99              @Override
100             public ChangeHandler<Object> build(String description) {
101                 if (description.equals("equals")) {
102                     return INSTANCE;
103                 }
104                 return null;
105             }
106         }
107 
108         private Equal() {
109         }
110 
111         @Override
112         public Result handle(@Nullable Object oldValue, @Nullable Object newValue) {
113             return Changed.forBoolean(Objects.equals(oldValue, newValue), newValue);
114         }
115     }
116 
117     /**
118      * Wraps another detector and only delegates to it if both values are of a
119      * certain type. If they aren't then it delegates to Equal. Doesn't perform
120      * null checking - wrap me in NullSafe or just use the nullAndTypeSafe
121      * static method to build me.
122      *
123      * @param <T> type on which the wrapped detector operates
124      */
125     class TypeSafe<T> implements NonnullChangeHandler<Object> {
126         /**
127          * Wraps a ChangeHandler in a null-safe, type-safe way.
128          */
129         static <T> ChangeHandler<Object> nullAndTypeSafe(Class<T> type, NonnullChangeHandler<T> delegate) {
130             return new ChangeHandler.NullSafe<>(new ChangeHandler.TypeSafe<>(type, delegate));
131         }
132 
133         private final Class<T> type;
134         private final NonnullChangeHandler<T> delegate;
135 
136         public TypeSafe(Class<T> type, NonnullChangeHandler<T> delegate) {
137             this.type = type;
138             this.delegate = delegate;
139         }
140 
141         @Override
142         public Result handle(@Nonnull Object oldValue, @Nonnull Object newValue) {
143             T oldValueCast;
144             T newValueCast;
145             try {
146                 oldValueCast = type.cast(oldValue);
147                 newValueCast = type.cast(newValue);
148             } catch (ClassCastException e) {
149                 return Equal.INSTANCE.handle(oldValue, newValue);
150             }
151             return delegate.handle(oldValueCast, newValueCast);
152         }
153     }
154 
155     class TypeSafeList<T> implements NonnullChangeHandler<Object> {
156         static <T> ChangeHandler<Object> nullAndTypeSafe(Class<T> type, NonnullChangeHandler<List<T>> delegate) {
157             return new NullSafe<>(new TypeSafeList<>(type, delegate));
158         }
159 
160         private final Class<T> type;
161         private final NonnullChangeHandler<List<T>> delegate;
162 
163         public TypeSafeList(Class<T> type, NonnullChangeHandler<List<T>> delegate) {
164             this.type = type;
165             this.delegate = delegate;
166         }
167 
168         @Override
169         public Result handle(@Nonnull Object oldList, @Nonnull Object newList) {
170             return delegate.handle(toTypedList(oldList), toTypedList(newList));
171         }
172 
173         @SuppressWarnings("unchecked")
174         @SuppressFBWarnings("PDP_POORLY_DEFINED_PARAMETER")
175         private List<T> toTypedList(Object value) {
176             List<T> list;
177             try {
178                 list = (List<T>)value;
179             } catch (ClassCastException e) {
180                 throw new IllegalArgumentException(String.format(Locale.ROOT,
181                         "Expected a list, but recieved (%s)", value.getClass().getName()), e);
182             }
183             if (!list.stream().allMatch(x -> type.isAssignableFrom(x.getClass()))) {
184                 throw new IllegalArgumentException(String.format(Locale.ROOT,
185                         "List elements not assignable to expected type (%s)", type.getName()));
186             }
187             return list;
188         }
189     }
190 
191     /**
192      * Result that shows that the old and new value are close enough that it
193      * isn't worth actually performing the update.
194      */
195     final class CloseEnough implements Result {
196         public static final Result INSTANCE = new CloseEnough();
197 
198         private CloseEnough() {
199             // Only a single instance is used
200         }
201 
202         @Override
203         public boolean isCloseEnough() {
204             return true;
205         }
206 
207         @Override
208         public Object newValue() {
209             return null;
210         }
211 
212         @Override
213         public boolean isDocumentNooped() {
214             return false;
215         }
216     }
217 
218     /**
219      * Result that shows that the entire document update should be
220      * canceled and turned into a noop.
221      */
222     final class NoopDocument implements Result {
223         public static final Result INSTANCE = new NoopDocument();
224 
225         public static Result forBoolean(boolean noop, Object newValue) {
226             if (noop) {
227                 return INSTANCE;
228             }
229             return new Changed(newValue);
230         }
231 
232         private NoopDocument() {
233             // Only a single instance is used
234         }
235 
236         @Override
237         public boolean isCloseEnough() {
238             return false;
239         }
240 
241         @Override
242         public Object newValue() {
243             return null;
244         }
245 
246         @Override
247         public boolean isDocumentNooped() {
248             return true;
249         }
250     }
251     /**
252      * Result that shows that the new value is different enough from the new
253      * value that its worth actually performing the update.
254      */
255     class Changed implements Result {
256         public static Result forBoolean(boolean closeEnough, @Nullable Object newValue) {
257             if (closeEnough) {
258                 return CloseEnough.INSTANCE;
259             }
260             return new Changed(newValue);
261         }
262 
263         @Nullable private final Object newValue;
264 
265         public Changed(@Nullable Object newValue) {
266             this.newValue = newValue;
267         }
268 
269         @Override
270         public boolean isCloseEnough() {
271             return false;
272         }
273 
274         @Override
275         public Object newValue() {
276             return newValue;
277         }
278 
279         @Override
280         public boolean isDocumentNooped() {
281             return false;
282         }
283     }
284 }