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 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   }