View Javadoc
1   package org.wikimedia.search.extra.superdetectnoop;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.Iterator;
6   import java.util.LinkedHashSet;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Optional;
10  import java.util.Set;
11  import java.util.stream.Collectors;
12  
13  import javax.annotation.Nullable;
14  
15  import com.google.common.collect.ImmutableSet;
16  
17  /**
18   * Implements Set-like behavior for lists.
19   */
20  public class SetHandler implements ChangeHandler<Object> {
21  
22      /**
23       * Singleton used by recognizer. The parameter values came from running
24       * SetHandlerMonteCarlo on a laptop so they aren't really theoretically
25       * sound, just reasonable guesses.
26       */
27      private static final ChangeHandler<Object> INSTANCE = new SetHandler(150, Integer.MAX_VALUE, 20);
28  
29      private static final String PARAM_ADD = "add";
30      private static final String PARAM_REMOVE = "remove";
31      private static final String PARAM_MAX_SIZE = "max_size";
32  
33      private  static final Set<String> VALID_PARAMS = ImmutableSet.of(PARAM_ADD, PARAM_REMOVE, PARAM_MAX_SIZE);
34  
35      public static class Recognizer implements ChangeHandler.Recognizer {
36          @Override
37          public ChangeHandler<Object> build(String description) {
38              if (description.equals("set")) {
39                  return INSTANCE;
40              }
41              return null;
42          }
43      }
44  
45      private final int minConvert;
46      private final int maxConvert;
47      private final int maxKeepAsList;
48  
49      public SetHandler(int minConvert, int maxConvert, int maxKeepAsList) {
50          this.minConvert = minConvert;
51          this.maxConvert = maxConvert;
52          this.maxKeepAsList = maxKeepAsList;
53      }
54  
55      @Override
56      @SuppressWarnings({"unchecked", "CyclomaticComplexity", "NPathComplexity"})
57      public ChangeHandler.Result handle(@Nullable Object oldValue, @Nullable Object newValue) {
58          if (newValue == null) {
59              return Changed.forBoolean(oldValue == null, null);
60          }
61          /*
62           * Note that if the old value isn't a list we just wrap it in one.
63           * That's _probably_ the right thing to do here.
64           */
65          Collection<Object> value = listify(oldValue);
66          Map<String, Object> params;
67          try {
68              params = ((Map<String, Object>) newValue);
69              final String excessiveParams = params.keySet().stream()
70                  .filter(key -> !VALID_PARAMS.contains(key)).collect(Collectors.joining(", "));
71              if (!excessiveParams.isEmpty()) {
72                  throw new IllegalArgumentException("Unexpected parameter(s) "
73                      + excessiveParams + "; expected "
74                      + String.join(", ", VALID_PARAMS));
75              }
76          } catch (ClassCastException e) {
77              throw new IllegalArgumentException("Expected parameters to be a map containing "
78                  + String.join(", ", VALID_PARAMS), e);
79          }
80          List<Object> remove = listify(params.get(PARAM_REMOVE));
81          List<Object> add = listify(params.get(PARAM_ADD))
82              .stream().filter(toAdd -> !remove.contains(toAdd))
83              .collect(Collectors.toList());
84  
85          int maxSize = Optional.ofNullable((Number) params.get(PARAM_MAX_SIZE)).map(Number::intValue)
86              .orElse(Integer.MAX_VALUE);
87  
88          if (add.size() + remove.size() > maxKeepAsList && minConvert < value.size() && value.size() < maxConvert) {
89              value = new LinkedHashSet<>(value);
90          }
91          boolean changed = value.removeAll(remove);
92          long remainingAddCount = Math.min(Math.max(0, maxSize - value.size()), add.size());
93  
94          final Iterator<Object> adderator = add.iterator();
95          while (remainingAddCount > 0 && adderator.hasNext()) {
96              final Object toAdd = adderator.next();
97              if (!value.contains(toAdd)) {
98                  value.add(toAdd);
99                  changed = true;
100                 --remainingAddCount;
101             }
102         }
103         if (!changed) {
104             return CloseEnough.INSTANCE;
105         }
106         return new Changed(value instanceof List ? value : new ArrayList<>(value));
107     }
108 
109     /**
110      * Converts a single value into a mutable list. If the value is already a
111      * list then just returns it without modification. If it was null then
112      * returns an empty list.
113      */
114     @SuppressWarnings("unchecked")
115     private List<Object> listify(@Nullable Object value) {
116         if (value == null) {
117             return new ArrayList<>();
118         }
119         if (value instanceof List) {
120             return (List<Object>) value;
121         }
122         List<Object> result = new ArrayList<>();
123         result.add(value);
124         return result;
125     }
126 }