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 package info.magnolia.admincentral.findbar.search;
35
36 import info.magnolia.periscope.Periscope;
37 import info.magnolia.periscope.SupplierAwareSearchResult;
38 import info.magnolia.periscope.search.SearchQuery;
39 import info.magnolia.periscope.search.SearchResultSupplier;
40
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.LinkedList;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Queue;
48 import java.util.function.BiConsumer;
49 import java.util.function.BiFunction;
50
51 import lombok.extern.slf4j.Slf4j;
52
53
54
55
56 @Slf4j
57 public class ResultCollector {
58
59 private static final int MAX_TOTAL_RESULTS = 100;
60
61 private final BiFunction<SearchQuery, List<SearchResultSupplier>,
62 List<Periscope.ResultsPromise>> searchEngine;
63 private final BiConsumer<SearchQuery, List<SupplierAwareSearchResult>> onChange;
64 private SearchQuery currentQuery;
65
66 public ResultCollector(BiFunction<SearchQuery, List<SearchResultSupplier>, List<Periscope.ResultsPromise>> searchEngine,
67 BiConsumer<SearchQuery, List<SupplierAwareSearchResult>> onChange) {
68 this.searchEngine = searchEngine;
69 this.onChange = onChange;
70 }
71
72 public void search(SearchQuery searchQuery, List<SearchResultSupplier> suppliers, Runnable onComplete) {
73 this.currentQuery = searchQuery;
74
75 List<SupplierAwareSearchResult> allResults = Collections.synchronizedList(new ArrayList<>());
76
77
78
79
80
81
82 Queue<SearchResultSupplier> missingSuppliers = new LinkedList<>(suppliers);
83 Map<SearchResultSupplier, List<SupplierAwareSearchResult>> retainedResults = new HashMap<>();
84
85 List<Periscope.ResultsPromise> completableFutures = searchEngine.apply(searchQuery, suppliers);
86
87 final Runnable triggerCallbacks = () -> {
88
89 if (allResults.isEmpty() && !missingSuppliers.isEmpty()) {
90 return;
91 }
92
93 try {
94 List<SupplierAwareSearchResult> subList = allResults.subList(0, Math.min(MAX_TOTAL_RESULTS, allResults.size()));
95 onChange.accept(searchQuery, new ArrayList<>(subList));
96 } catch (Exception e) {
97 log.error("Updating result failed", e);
98 }
99
100 if (missingSuppliers.isEmpty()) {
101 onComplete.run();
102 }
103 };
104
105 for (Periscope.ResultsPromise promise : completableFutures) {
106 promise.getResultsFuture().whenCompleteAsync((results, exception) -> {
107 synchronized (this) {
108
109
110 if (searchQuery != this.currentQuery || allResults.size() >= MAX_TOTAL_RESULTS) {
111 return;
112 }
113
114 if (exception != null) {
115 log.error("An error occurred during the search process, therefore an empty collection will be returned.", exception);
116 results = Collections.emptyList();
117 }
118
119 if (promise.getSupplier() == null) {
120 log.error("Results promise has no supplier - result listing might break because of this");
121 throw new IllegalStateException("Results promise has no supplier");
122 }
123
124 if (promise.getSupplier().equals(missingSuppliers.peek())) {
125 allResults.addAll(results);
126 missingSuppliers.poll();
127 } else {
128 retainedResults.put(promise.getSupplier(), results);
129 }
130
131
132 while (retainedResults.containsKey(missingSuppliers.peek())) {
133 allResults.addAll(retainedResults.remove(missingSuppliers.poll()));
134 }
135
136 if (allResults.size() >= MAX_TOTAL_RESULTS) {
137 missingSuppliers.clear();
138 }
139
140 triggerCallbacks.run();
141 }
142 });
143 }
144 }
145 }