1 /*
2 * *************************************************************************************************************************************************************
3 *
4 * TheseFoolishThings: Miscellaneous utilities
5 * http://tidalwave.it/projects/thesefoolishthings
6 *
7 * Copyright (C) 2009 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8 *
9 * *************************************************************************************************************************************************************
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12 * You may obtain a copy of the License at
13 *
14 * http://www.apache.org/licenses/LICENSE-2.0
15 *
16 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18 *
19 * *************************************************************************************************************************************************************
20 *
21 * git clone https://bitbucket.org/tidalwave/thesefoolishthings-src
22 * git clone https://github.com/tidalwave-it/thesefoolishthings-src
23 *
24 * *************************************************************************************************************************************************************
25 */
26 package it.tidalwave.util.spi;
27
28 import java.lang.reflect.Constructor;
29 // import javax.annotation.Nonnegative;
30 import jakarta.annotation.Nonnull;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.concurrent.CopyOnWriteArrayList;
35 import it.tidalwave.util.Finder;
36 import lombok.AccessLevel;
37 import lombok.AllArgsConstructor;
38 import lombok.Getter;
39 import lombok.RequiredArgsConstructor;
40 import lombok.ToString;
41 import lombok.extern.slf4j.Slf4j;
42 import static it.tidalwave.util.CollectionUtils.concat;
43
44 /***************************************************************************************************************************************************************
45 *
46 * A support class for implementing a {@link Finder}. Subclasses only need to implement the {@link #computeResults()} method where <i>raw</i> results are
47 * retrieved (with raw we mean that they shouldn't be filtered or sorted, as post-processing will be performed by this class) and a clone constructor.
48 * If you don't need to extend the {@link Finder} with extra methods, please use the simplified {@link SimpleFinderSupport}.
49 *
50 * @author Fabrizio Giudici
51 *
52 **************************************************************************************************************************************************************/
53 @Slf4j @AllArgsConstructor(access = AccessLevel.PRIVATE) @ToString
54 public class HierarchicFinderSupport<T, F extends Finder<T>> implements Finder<T>
55 {
56 private static final long serialVersionUID = 2467809593956684L;
57
58 @RequiredArgsConstructor
59 static class Sorter<U>
60 {
61 @Nonnull
62 private final InMemorySortCriterion<U> sortCriterion;
63
64 @Nonnull
65 private final SortDirection sortDirection;
66
67 public void sort (@Nonnull final List<? extends U> results)
68 {
69 sortCriterion.sort(results, sortDirection);
70 }
71 }
72
73 private static final String MESSAGE =
74 "Since version 2.0, Implementations of Finder must have a clone constructor such as "
75 + "MyFinder(MyFinder other, Object override). This means that they can't be implemented by anonymous or inner, "
76 + "non static classes. See the javadoc for further information. Could not find constructor: ";
77
78 @Nonnull
79 private final String name;
80
81 /* @Nonnegative */
82 protected final int firstResult;
83
84 /* @Nonnegative */
85 protected final int maxResults;
86
87 @Nonnull @Getter(AccessLevel.PROTECTED)
88 private final List<Object> contexts;
89
90 @Nonnull
91 private final List<Sorter<T>> sorters;
92
93 private static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE;
94
95 /***********************************************************************************************************************************************************
96 * Creates an instance with the given name (that will be used for diagnostics).
97 * @param name the name
98 **********************************************************************************************************************************************************/
99 protected HierarchicFinderSupport (@Nonnull final String name)
100 {
101 this.name = name;
102 this.firstResult = 0;
103 this.maxResults = DEFAULT_MAX_RESULTS;
104 this.sorters = new ArrayList<>();
105 this.contexts = Collections.emptyList();
106 checkSubClass();
107 }
108
109 /***********************************************************************************************************************************************************
110 * Default constructor.
111 **********************************************************************************************************************************************************/
112 protected HierarchicFinderSupport()
113 {
114 this.name = getClass().getName();
115 this.firstResult = 0;
116 this.maxResults = DEFAULT_MAX_RESULTS;
117 this.sorters = new ArrayList<>();
118 this.contexts = Collections.emptyList();
119 checkSubClass();
120 }
121
122 /***********************************************************************************************************************************************************
123 * Clone constructor for subclasses.
124 * @param other the other instance to clone
125 * @param holder the holder object
126 **********************************************************************************************************************************************************/
127 @SuppressWarnings("unchecked")
128 protected HierarchicFinderSupport (@Nonnull final HierarchicFinderSupport<T, F> other, @Nonnull final Object holder)
129 {
130 log.trace("HierarchicFinderSupport({}, {})", other, holder);
131 final var source = getSource(HierarchicFinderSupport.class, other, holder);
132 this.name = source.name;
133 this.firstResult = source.firstResult;
134 this.maxResults = source.maxResults;
135 this.sorters = source.sorters;
136 this.contexts = source.contexts; // it's always unmodifiable
137 }
138
139 /***********************************************************************************************************************************************************
140 * This method throws an exception since a {@code Finder} extending this class must be cloned with {@link #clonedWith(Object)}.
141 * @see #clonedWith(Object)
142 * @deprecated
143 **********************************************************************************************************************************************************/
144 @Override @Nonnull
145 public final HierarchicFinderSupport<T, F> clone()
146 {
147 throw new UnsupportedOperationException("HierarchicFinderSupport.clone() no more supported");
148 }
149
150 /***********************************************************************************************************************************************************
151 * Create a clone of this object calling the special copy constructor by reflection.
152 * @param override the override object
153 * @return the clone
154 **********************************************************************************************************************************************************/
155 @Nonnull @SuppressWarnings("unchecked")
156 protected F clonedWith (@Nonnull final Object override)
157 {
158 try
159 {
160 final var constructor = getCloneConstructor();
161 constructor.setAccessible(true);
162 return (F)constructor.newInstance(this, override);
163 }
164 catch (Exception e)
165 {
166 throw new RuntimeException(e);
167 }
168 }
169
170 /***********************************************************************************************************************************************************
171 * Create a clone of this object calling the special clone constructor by reflection.
172 * @param override the override object
173 * @return the clone
174 * @deprecated Use {@link #clonedWith(Object)} instead.
175 **********************************************************************************************************************************************************/
176 @Nonnull @Deprecated
177 protected F clone (@Nonnull final Object override)
178 {
179 return clonedWith(override);
180 }
181
182 /***********************************************************************************************************************************************************
183 * {@inheritDoc}
184 **********************************************************************************************************************************************************/
185 @Override @Nonnull
186 public F from (/* @Nonnegative */ final int firstResult)
187 {
188 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
189 }
190
191 /***********************************************************************************************************************************************************
192 * {@inheritDoc}
193 **********************************************************************************************************************************************************/
194 @Override @Nonnull
195 public F max (/* @Nonnegative */ final int maxResults)
196 {
197 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
198 }
199
200 /***********************************************************************************************************************************************************
201 * {@inheritDoc}
202 **********************************************************************************************************************************************************/
203 @Override @Nonnull
204 public F withContext (@Nonnull final Object context)
205 {
206 final var contexts = concat(this.contexts, context);
207 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
208 }
209
210 /***********************************************************************************************************************************************************
211 * {@inheritDoc}
212 **********************************************************************************************************************************************************/
213 @Override @Nonnull
214 public <U> Finder<U> ofType (@Nonnull final Class<U> type)
215 {
216 throw new UnsupportedOperationException("Must be eventually implemented by subclasses.");
217 }
218
219 /***********************************************************************************************************************************************************
220 * {@inheritDoc}
221 **********************************************************************************************************************************************************/
222 @Override @Nonnull
223 public F sort (@Nonnull final SortCriterion criterion, @Nonnull final SortDirection direction)
224 {
225 if (criterion instanceof Finder.InMemorySortCriterion)
226 {
227 @SuppressWarnings("unchecked")
228 final var sorters = concat(this.sorters, new Sorter<>((InMemorySortCriterion<T>)criterion, direction));
229 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
230 }
231
232 final var template = "%s does not implement %s - you need to subclass Finder and override sort()";
233 final var message = String.format(template, criterion, InMemorySortCriterion.class);
234 throw new UnsupportedOperationException(message);
235 }
236
237 /***********************************************************************************************************************************************************
238 * {@inheritDoc}
239 **********************************************************************************************************************************************************/
240 @Override @Nonnull
241 public final F sort (@Nonnull final SortCriterion criterion)
242 {
243 return sort(criterion, SortDirection.ASCENDING);
244 }
245
246 /***********************************************************************************************************************************************************
247 * {@inheritDoc}
248 **********************************************************************************************************************************************************/
249 @Override @Nonnull
250 public List<T> results()
251 {
252 return computeNeededResults();
253 }
254
255 /***********************************************************************************************************************************************************
256 * {@inheritDoc}
257 **********************************************************************************************************************************************************/
258 @Override /* @Nonnegative */
259 public int count()
260 {
261 return computeNeededResults().size();
262 }
263
264 /***********************************************************************************************************************************************************
265 * Subclasses can implement this method where *all* the raw results must be actually retrieved.
266 * @return the unprocessed results
267 **********************************************************************************************************************************************************/
268 // START SNIPPET: computeResults
269 @Nonnull
270 protected List<T> computeResults()
271 // END SNIPPET: computeResults
272 {
273 throw new UnsupportedOperationException("You must implement me!");
274 }
275
276 /***********************************************************************************************************************************************************
277 * Subclasses can implement this method where *only the requested* raw results must be retrieved.
278 * @return the unprocessed results
279 **********************************************************************************************************************************************************/
280 // START SNIPPET: computeNeededResults
281 @Nonnull
282 protected List<T> computeNeededResults()
283 // END SNIPPET: computeNeededResults
284 {
285 log.trace("computeNeededResults() - {}", this);
286 var results = computeResults();
287
288 // First sort and then extract the sublist
289 for (final var sorter : sorters)
290 {
291 log.trace(">>>> sorting with {}...", sorter);
292 sorter.sort(results);
293 }
294
295 final var toIndex = (int)Math.min(results.size(), (long)firstResult + (long)maxResults);
296
297 if (firstResult > toIndex)
298 {
299 return new CopyOnWriteArrayList<>();
300 }
301
302 results = results.subList(firstResult, toIndex);
303
304 return results;
305 }
306
307 /***********************************************************************************************************************************************************
308 * A utility method used by the copy constructor (see general documentation). If the override object is strictly of the specified type, it is returned;
309 * otherwise the other object is returned.
310 * @param <U> the static type of the source
311 * @param type the dynamic type of the source
312 * @param other the other finder
313 * @param override the holder object
314 * @return the override or other
315 **********************************************************************************************************************************************************/
316 @Nonnull
317 protected static <U> U getSource (@Nonnull final Class<? extends U> type,
318 @Nonnull final U other,
319 @Nonnull final Object override)
320 {
321 return override.getClass().equals(type) ? type.cast(override) : other;
322 }
323
324 /***********************************************************************************************************************************************************
325 *
326 **********************************************************************************************************************************************************/
327 @Nonnull @SuppressWarnings("unchecked")
328 private Constructor<HierarchicFinderSupport<T, F>> getCloneConstructor()
329 throws SecurityException, NoSuchMethodException
330 {
331 return (Constructor<HierarchicFinderSupport<T, F>>)getClass().getConstructor(getClass(), Object.class);
332 }
333
334 /***********************************************************************************************************************************************************
335 *
336 **********************************************************************************************************************************************************/
337 private void checkSubClass ()
338 {
339 try
340 {
341 getCloneConstructor();
342 }
343 catch (SecurityException | NoSuchMethodException e)
344 {
345 throw new ExceptionInInitializerError(MESSAGE + e.getMessage());
346 }
347 }
348 }