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
19
20
21
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
79
80
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
96 if (current != null && current.equals(updated)) {
97 continue;
98 }
99 if (!isDeleteMarker(entry.getKey(), updated)) {
100
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 }