View Javadoc
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 javax.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             final var sorters = concat(this.sorters, new Sorter<>((InMemorySortCriterion<T>)criterion, direction));
237             return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
238           }
239 
240         final var template = "%s does not implement %s - you need to subclass Finder and override sort()";
241         final var message = String.format(template, criterion, InMemorySortCriterion.class);
242         throw new UnsupportedOperationException(message);
243       }
244 
245     /***********************************************************************************************************************************************************
246      * {@inheritDoc}
247      **********************************************************************************************************************************************************/
248     @Override @Nonnull
249     public final F sort (@Nonnull final SortCriterion criterion)
250       {
251         return sort(criterion, SortDirection.ASCENDING);
252       }
253 
254     /***********************************************************************************************************************************************************
255      * {@inheritDoc}
256      **********************************************************************************************************************************************************/
257     @Override @Nonnull
258     public List<T> results()
259       {
260         return computeNeededResults();
261       }
262 
263     /***********************************************************************************************************************************************************
264      * {@inheritDoc}
265      **********************************************************************************************************************************************************/
266     @Override @Nonnegative
267     public int count()
268       {
269         return computeNeededResults().size();
270       }
271 
272     /***********************************************************************************************************************************************************
273      * Subclasses can implement this method where *all* the raw results must be actually retrieved.
274      *
275      * @return  the unprocessed results
276      **********************************************************************************************************************************************************/
277     // START SNIPPET: computeResults
278     @Nonnull
279     protected List<T> computeResults()
280     // END SNIPPET: computeResults
281       {
282         throw new UnsupportedOperationException("You must implement me!");
283       }
284 
285     /***********************************************************************************************************************************************************
286      * Subclasses can implement this method where *only the requested* raw results must be retrieved.
287      *
288      * @return  the unprocessed results
289      **********************************************************************************************************************************************************/
290     // START SNIPPET: computeNeededResults
291     @Nonnull
292     protected List<T> computeNeededResults()
293     // END SNIPPET: computeNeededResults
294       {
295         log.trace("computeNeededResults() - {}", this);
296         var results = computeResults();
297 
298         // First sort and then extract the sublist
299         for (final var sorter : sorters)
300           {
301             log.trace(">>>> sorting with {}...", sorter);
302             sorter.sort(results);
303           }
304 
305         final var toIndex = (int)Math.min(results.size(), (long)firstResult + (long)maxResults);
306 
307         if (firstResult > toIndex)
308           {
309             return new CopyOnWriteArrayList<>();
310           }
311 
312         results = results.subList(firstResult, toIndex);
313 
314         return results;
315       }
316 
317     /***********************************************************************************************************************************************************
318      * A utility method used by the copy constructor (see general documentation). If the override object is strictly
319      * of the specified type, it is returned; otherwise the other object is returned.
320      *
321      * @param <U>       the static type of the source
322      * @param type      the dynamic type of the source
323      * @param other     the other finder
324      * @param override  the holder object
325      * @return          the override or other
326      **********************************************************************************************************************************************************/
327     @Nonnull
328     protected static <U> U getSource (@Nonnull final Class<? extends U> type,
329                                       @Nonnull final U other,
330                                       @Nonnull final Object override)
331       {
332         return override.getClass().equals(type) ? type.cast(override) : other;
333       }
334 
335     /***********************************************************************************************************************************************************
336      **********************************************************************************************************************************************************/
337     @Nonnull
338     private Constructor<? extends HierarchicFinderSupport> getCloneConstructor()
339       throws SecurityException, NoSuchMethodException
340       {
341         return getClass().getConstructor(getClass(), Object.class);
342       }
343 
344     /***********************************************************************************************************************************************************
345      **********************************************************************************************************************************************************/
346     private void checkSubClass ()
347       {
348         try
349           {
350             getCloneConstructor();
351           }
352         catch (SecurityException | NoSuchMethodException e)
353           {
354             throw new ExceptionInInitializerError(MESSAGE + e.getMessage());
355           }
356       }
357   }