View Javadoc
1   package org.wikimedia.search.extra.superdetectnoop;
2   
3   import static java.lang.Boolean.FALSE;
4   import static java.util.stream.Collectors.groupingBy;
5   import static java.util.stream.Collectors.toList;
6   import static java.util.stream.Collectors.toSet;
7   
8   import java.util.Collection;
9   import java.util.Iterator;
10  import java.util.List;
11  import java.util.Map;
12  import java.util.Optional;
13  import java.util.Set;
14  
15  import javax.annotation.Nonnull;
16  
17  /**
18   * Implementation of {@link ChangeHandler} that allows for maintaining multiple
19   * sets inside a single list stored within Elasticsearch. The sub sets are
20   * updated in their entirety, while unreferenced sets in the source are maintained.
21   * Sets can be removed by providing a single tombstone value, __DELETE_GROUPING__.
22   */
23  public class MultiListHandler implements ChangeHandler.NonnullChangeHandler<List<String>> {
24      static final String DELETE = "__DELETE_GROUPING__";
25      static final ChangeHandler<Object> INSTANCE =
26              ChangeHandler.TypeSafeList.nullAndTypeSafe(String.class, new MultiListHandler());
27  
28      public static final ChangeHandler.Recognizer RECOGNIZER = desc ->
29              desc.equals("multilist") ? INSTANCE : null;
30  
31      @Override
32      public ChangeHandler.Result handle(@Nonnull List<String> oldValue, @Nonnull List<String> newValue) {
33          if (newValue.isEmpty()) {
34              throw new IllegalArgumentException("Empty update provided to MultiListHandler");
35          }
36          MultiSet original = MultiSet.parse(oldValue);
37          MultiSet update = MultiSet.parse(newValue);
38          if (original.replaceFrom(update)) {
39              return new ChangeHandler.Changed(original.flatten());
40          } else {
41              return ChangeHandler.CloseEnough.INSTANCE;
42          }
43      }
44  
45      private static final class MultiSet {
46          private static final char DELIMITER = '/';
47          private static final String UNNAMED = "__UNNAMED_GROUPING__";
48          private final Map<String, Set<String>> sets;
49  
50          private MultiSet(Map<String, Set<String>> sets) {
51              this.sets = sets;
52          }
53  
54          static MultiSet parse(Collection<String> strings) {
55              return new MultiSet(strings.stream()
56                      .collect(groupingBy(val -> {
57                          int pos = val.indexOf(DELIMITER);
58                          return pos == -1 ? UNNAMED : val.substring(0, pos);
59                      }, toSet())));
60          }
61  
62          private <T> Optional<T> onlyElement(Collection<T> foo) {
63              Iterator<T> it = foo.iterator();
64              if (!it.hasNext()) {
65                  return Optional.empty();
66              }
67              T value = it.next();
68              if (it.hasNext()) {
69                  return Optional.empty();
70              } else {
71                  return Optional.of(value);
72              }
73          }
74  
75          private boolean isDeleteMarker(String group, Set<String> values) {
76              return onlyElement(values)
77                  .map(value -> {
78                      // We don't need to verify the group, by construction the prefix must match
79                      // the group. We only need to verify that there isn't additional content. Verify
80                      // by ensuring there is only enough room for the marker and the prefix.
81                      int expectedLength = DELETE.length();
82                      if (!UNNAMED.equals(group)) {
83                          expectedLength += 1 + group.length();
84                      }
85                      return value.length() == expectedLength && value.endsWith(DELETE);
86                  })
87                  .orElse(FALSE);
88          }
89  
90          boolean replaceFrom(MultiSet other) {
91              boolean changed = false;
92              for (Map.Entry<String, Set<String>> entry : other.sets.entrySet()) {
93                  Set<String> current = sets.get(entry.getKey());
94                  Set<String> updated = entry.getValue();
95                  // If we are already holding an equivalent value skip the update.
96                  if (current != null && current.equals(updated)) {
97                      continue;
98                  }
99                  if (!isDeleteMarker(entry.getKey(), updated)) {
100                     // Standard update
101                     changed = true;
102                     sets.put(entry.getKey(), updated);
103                 } else if (sets.remove(entry.getKey()) != null) {
104                     changed = true;
105                 }
106             }
107             return changed;
108         }
109 
110         List<String> flatten() {
111             return sets.entrySet().stream()
112                 .flatMap(entry -> entry.getValue().stream())
113                 .collect(toList());
114         }
115     }
116 }