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()}
47 * method where <i>raw</i> results are retrieved (with raw we mean that they shouldn't be filtered or sorted, as
48 * post-processing will be performed by this class) and a clone constructor.
49 *
50 * If you don't need to extend the {@link Finder} with extra methods, please use the simplified
51 * {@link SimpleFinderSupport}.
52 *
53 * @author Fabrizio Giudici
54 * @it.tidalwave.javadoc.draft
55 *
56 **************************************************************************************************************************************************************/
57 @Slf4j @AllArgsConstructor(access = AccessLevel.PRIVATE) @ToString
58 public class HierarchicFinderSupport<T, F extends Finder<T>> implements Finder<T>
59 {
60 private static final long serialVersionUID = 2467809593956684L;
61
62 @RequiredArgsConstructor
63 static class Sorter<U>
64 {
65 @Nonnull
66 private final InMemorySortCriterion<U> sortCriterion;
67
68 @Nonnull
69 private final SortDirection sortDirection;
70
71 public void sort (@Nonnull final List<? extends U> results)
72 {
73 sortCriterion.sort(results, sortDirection);
74 }
75 }
76
77 private static final String MESSAGE =
78 "Since version 2.0, Implementations of Finder must have a clone constructor such as "
79 + "MyFinder(MyFinder other, Object override). This means that they can't be implemented by anonymous or inner, "
80 + "non static classes. See the javadoc for further information. Could not find constructor: ";
81
82 @Nonnull
83 private final String name;
84
85 /* @Nonnegative */
86 protected final int firstResult;
87
88 /* @Nonnegative */
89 protected final int maxResults;
90
91 @Nonnull @Getter(AccessLevel.PROTECTED)
92 private final List<Object> contexts;
93
94 @Nonnull
95 private final List<Sorter<T>> sorters;
96
97 private static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE;
98
99 /***********************************************************************************************************************************************************
100 * Creates an instance with the given name (that will be used for diagnostics).
101 *
102 * @param name the name
103 **********************************************************************************************************************************************************/
104 protected HierarchicFinderSupport (@Nonnull final String name)
105 {
106 this.name = name;
107 this.firstResult = 0;
108 this.maxResults = DEFAULT_MAX_RESULTS;
109 this.sorters = new ArrayList<>();
110 this.contexts = Collections.emptyList();
111 checkSubClass();
112 }
113
114 /***********************************************************************************************************************************************************
115 * Default constructor.
116 **********************************************************************************************************************************************************/
117 protected HierarchicFinderSupport()
118 {
119 this.name = getClass().getName();
120 this.firstResult = 0;
121 this.maxResults = DEFAULT_MAX_RESULTS;
122 this.sorters = new ArrayList<>();
123 this.contexts = Collections.emptyList();
124 checkSubClass();
125 }
126
127 /***********************************************************************************************************************************************************
128 * Clone constructor for subclasses.
129 *
130 * @param other the other instance to clone
131 * @param holder the holder object
132 **********************************************************************************************************************************************************/
133 protected HierarchicFinderSupport (@Nonnull final HierarchicFinderSupport<T, F> other, @Nonnull final Object holder)
134 {
135 log.trace("HierarchicFinderSupport({}, {})", other, holder);
136 final var source = getSource(HierarchicFinderSupport.class, other, holder);
137 this.name = source.name;
138 this.firstResult = source.firstResult;
139 this.maxResults = source.maxResults;
140 this.sorters = source.sorters;
141 this.contexts = source.contexts; // it's always unmodifiable
142 }
143
144 /***********************************************************************************************************************************************************
145 * This method throws an exception since a {@code Finder} extending this class must be cloned with
146 * {@link #clonedWith(Object)}.
147 *
148 * @see #clonedWith(Object)
149 * @deprecated
150 **********************************************************************************************************************************************************/
151 @Override @Nonnull
152 public final HierarchicFinderSupport<T, F> clone()
153 {
154 throw new UnsupportedOperationException("\"HierarchicFinderSupport.clone() no more supported");
155 }
156
157 /***********************************************************************************************************************************************************
158 * Create a clone of this object calling the special copy constructor by reflection.
159 *
160 * @param override the override object
161 * @return the clone
162 **********************************************************************************************************************************************************/
163 @Nonnull
164 protected F clonedWith (@Nonnull final Object override)
165 {
166 try
167 {
168 final var constructor = getCloneConstructor();
169 constructor.setAccessible(true);
170 return (F)constructor.newInstance(this, override);
171 }
172 catch (Exception e)
173 {
174 throw new RuntimeException(e);
175 }
176 }
177
178 /***********************************************************************************************************************************************************
179 * Create a clone of this object calling the special clone constructor by reflection.
180 *
181 * @param override the override object
182 * @return the clone
183 * @deprecated Use {@link #clonedWith(Object)} instead.
184 **********************************************************************************************************************************************************/
185 @Nonnull @Deprecated
186 protected F clone (@Nonnull final Object override)
187 {
188 return clonedWith(override);
189 }
190
191 /***********************************************************************************************************************************************************
192 * {@inheritDoc}
193 **********************************************************************************************************************************************************/
194 @Override @Nonnull
195 public F from (/* @Nonnegative */ final int firstResult)
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 max (/* @Nonnegative */ final int maxResults)
205 {
206 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
207 }
208
209 /***********************************************************************************************************************************************************
210 * {@inheritDoc}
211 **********************************************************************************************************************************************************/
212 @Override @Nonnull
213 public F withContext (@Nonnull final Object context)
214 {
215 final var contexts = concat(this.contexts, context);
216 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
217 }
218
219 /***********************************************************************************************************************************************************
220 * {@inheritDoc}
221 **********************************************************************************************************************************************************/
222 @Override @Nonnull
223 public <U> Finder<U> ofType (@Nonnull final Class<U> type)
224 {
225 throw new UnsupportedOperationException("Must be eventually implemented by subclasses.");
226 }
227
228 /***********************************************************************************************************************************************************
229 * {@inheritDoc}
230 **********************************************************************************************************************************************************/
231 @Override @Nonnull
232 public F sort (@Nonnull final SortCriterion criterion, @Nonnull final SortDirection direction)
233 {
234 if (criterion instanceof Finder.InMemorySortCriterion)
235 {
236 @SuppressWarnings("unchecked")
237 final var sorters = concat(this.sorters, new Sorter<>((InMemorySortCriterion<T>)criterion, direction));
238 return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
239 }
240
241 final var template = "%s does not implement %s - you need to subclass Finder and override sort()";
242 final var message = String.format(template, criterion, InMemorySortCriterion.class);
243 throw new UnsupportedOperationException(message);
244 }
245
246 /***********************************************************************************************************************************************************
247 * {@inheritDoc}
248 **********************************************************************************************************************************************************/
249 @Override @Nonnull
250 public final F sort (@Nonnull final SortCriterion criterion)
251 {
252 return sort(criterion, SortDirection.ASCENDING);
253 }
254
255 /***********************************************************************************************************************************************************
256 * {@inheritDoc}
257 **********************************************************************************************************************************************************/
258 @Override @Nonnull
259 public List<T> results()
260 {
261 return computeNeededResults();
262 }
263
264 /***********************************************************************************************************************************************************
265 * {@inheritDoc}
266 **********************************************************************************************************************************************************/
267 @Override /* @Nonnegative */
268 public int count()
269 {
270 return computeNeededResults().size();
271 }
272
273 /***********************************************************************************************************************************************************
274 * Subclasses can implement this method where *all* the raw results must be actually retrieved.
275 *
276 * @return the unprocessed results
277 **********************************************************************************************************************************************************/
278 // START SNIPPET: computeResults
279 @Nonnull
280 protected List<T> computeResults()
281 // END SNIPPET: computeResults
282 {
283 throw new UnsupportedOperationException("You must implement me!");
284 }
285
286 /***********************************************************************************************************************************************************
287 * Subclasses can implement this method where *only the requested* raw results must be retrieved.
288 *
289 * @return the unprocessed results
290 **********************************************************************************************************************************************************/
291 // START SNIPPET: computeNeededResults
292 @Nonnull
293 protected List<T> computeNeededResults()
294 // END SNIPPET: computeNeededResults
295 {
296 log.trace("computeNeededResults() - {}", this);
297 var results = computeResults();
298
299 // First sort and then extract the sublist
300 for (final var sorter : sorters)
301 {
302 log.trace(">>>> sorting with {}...", sorter);
303 sorter.sort(results);
304 }
305
306 final var toIndex = (int)Math.min(results.size(), (long)firstResult + (long)maxResults);
307
308 if (firstResult > toIndex)
309 {
310 return new CopyOnWriteArrayList<>();
311 }
312
313 results = results.subList(firstResult, toIndex);
314
315 return results;
316 }
317
318 /***********************************************************************************************************************************************************
319 * A utility method used by the copy constructor (see general documentation). If the override object is strictly
320 * of the specified type, it is returned; otherwise the other object is returned.
321 *
322 * @param <U> the static type of the source
323 * @param type the dynamic type of the source
324 * @param other the other finder
325 * @param override the holder object
326 * @return the override or other
327 **********************************************************************************************************************************************************/
328 @Nonnull
329 protected static <U> U getSource (@Nonnull final Class<? extends U> type,
330 @Nonnull final U other,
331 @Nonnull final Object override)
332 {
333 return override.getClass().equals(type) ? type.cast(override) : other;
334 }
335
336 /***********************************************************************************************************************************************************
337 **********************************************************************************************************************************************************/
338 @Nonnull @SuppressWarnings("unchecked")
339 private Constructor<HierarchicFinderSupport<T, F>> getCloneConstructor()
340 throws SecurityException, NoSuchMethodException
341 {
342 return (Constructor<HierarchicFinderSupport<T, F>>)getClass().getConstructor(getClass(), Object.class);
343 }
344
345 /***********************************************************************************************************************************************************
346 **********************************************************************************************************************************************************/
347 private void checkSubClass ()
348 {
349 try
350 {
351 getCloneConstructor();
352 }
353 catch (SecurityException | NoSuchMethodException e)
354 {
355 throw new ExceptionInInitializerError(MESSAGE + e.getMessage());
356 }
357 }
358 }