HierarchicFinderSupport.java

  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. import java.lang.reflect.Constructor;
  28. // import javax.annotation.Nonnegative;
  29. import jakarta.annotation.Nonnull;
  30. import java.util.ArrayList;
  31. import java.util.Collections;
  32. import java.util.List;
  33. import java.util.concurrent.CopyOnWriteArrayList;
  34. import it.tidalwave.util.Finder;
  35. import lombok.AccessLevel;
  36. import lombok.AllArgsConstructor;
  37. import lombok.Getter;
  38. import lombok.RequiredArgsConstructor;
  39. import lombok.ToString;
  40. import lombok.extern.slf4j.Slf4j;
  41. import static it.tidalwave.util.CollectionUtils.concat;

  42. /***************************************************************************************************************************************************************
  43.  *
  44.  * A support class for implementing a {@link Finder}. Subclasses only need to implement the {@link #computeResults()} method where <i>raw</i> results are
  45.  * 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.
  46.  * If you don't need to extend the {@link Finder} with extra methods, please use the simplified {@link SimpleFinderSupport}.
  47.  *
  48.  * @author Fabrizio Giudici
  49.  *
  50.  **************************************************************************************************************************************************************/
  51. @Slf4j @AllArgsConstructor(access = AccessLevel.PRIVATE) @ToString
  52. public class HierarchicFinderSupport<T, F extends Finder<T>> implements Finder<T>
  53.   {
  54.     private static final long serialVersionUID = 2467809593956684L;

  55.     @RequiredArgsConstructor
  56.     static class Sorter<U>
  57.       {
  58.         @Nonnull
  59.         private final InMemorySortCriterion<U> sortCriterion;

  60.         @Nonnull
  61.         private final SortDirection sortDirection;

  62.         public void sort (@Nonnull final List<? extends U> results)
  63.           {
  64.             sortCriterion.sort(results, sortDirection);
  65.           }
  66.       }

  67.     private static final String MESSAGE =
  68.           "Since version 2.0, Implementations of Finder must have a clone constructor such as "
  69.         + "MyFinder(MyFinder other, Object override). This means that they can't be implemented by anonymous or inner, "
  70.         + "non static classes. See the javadoc for further information. Could not find constructor: ";

  71.     @Nonnull
  72.     private final String name;

  73.     /* @Nonnegative */
  74.     protected final int firstResult;

  75.     /* @Nonnegative */
  76.     protected final int maxResults;

  77.     @Nonnull @Getter(AccessLevel.PROTECTED)
  78.     private final List<Object> contexts;

  79.     @Nonnull
  80.     private final List<Sorter<T>> sorters;

  81.     private static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE;

  82.     /***********************************************************************************************************************************************************
  83.      * Creates an instance with the given name (that will be used for diagnostics).
  84.      * @param  name   the name
  85.      **********************************************************************************************************************************************************/
  86.     protected HierarchicFinderSupport (@Nonnull final String name)
  87.       {
  88.         this.name = name;
  89.         this.firstResult = 0;
  90.         this.maxResults = DEFAULT_MAX_RESULTS;
  91.         this.sorters = new ArrayList<>();
  92.         this.contexts = Collections.emptyList();
  93.         checkSubClass();
  94.       }

  95.     /***********************************************************************************************************************************************************
  96.      * Default constructor.
  97.      **********************************************************************************************************************************************************/
  98.     protected HierarchicFinderSupport()
  99.       {
  100.         this.name = getClass().getName();
  101.         this.firstResult = 0;
  102.         this.maxResults = DEFAULT_MAX_RESULTS;
  103.         this.sorters = new ArrayList<>();
  104.         this.contexts = Collections.emptyList();
  105.         checkSubClass();
  106.       }

  107.     /***********************************************************************************************************************************************************
  108.      * Clone constructor for subclasses.
  109.      * @param   other     the other instance to clone
  110.      * @param   holder    the holder object
  111.      **********************************************************************************************************************************************************/
  112.     @SuppressWarnings("unchecked")
  113.     protected HierarchicFinderSupport (@Nonnull final HierarchicFinderSupport<T, F> other, @Nonnull final Object holder)
  114.       {
  115.         log.trace("HierarchicFinderSupport({}, {})", other, holder);
  116.         final var source = getSource(HierarchicFinderSupport.class, other, holder);
  117.         this.name = source.name;
  118.         this.firstResult = source.firstResult;
  119.         this.maxResults = source.maxResults;
  120.         this.sorters = source.sorters;
  121.         this.contexts = source.contexts; // it's always unmodifiable
  122.       }

  123.     /***********************************************************************************************************************************************************
  124.      * This method throws an exception since a {@code Finder} extending this class must be cloned with {@link #clonedWith(Object)}.
  125.      * @see #clonedWith(Object)
  126.      * @deprecated
  127.      **********************************************************************************************************************************************************/
  128.     @Override @Nonnull
  129.     public final HierarchicFinderSupport<T, F> clone()
  130.       {
  131.         throw new UnsupportedOperationException("HierarchicFinderSupport.clone() no more supported");
  132.       }

  133.     /***********************************************************************************************************************************************************
  134.      * Create a clone of this object calling the special copy constructor by reflection.
  135.      * @param   override  the override object
  136.      * @return            the clone
  137.      **********************************************************************************************************************************************************/
  138.     @Nonnull @SuppressWarnings("unchecked")
  139.     protected F clonedWith (@Nonnull final Object override)
  140.       {
  141.         try
  142.           {
  143.             final var constructor = getCloneConstructor();
  144.             constructor.setAccessible(true);
  145.             return (F)constructor.newInstance(this, override);
  146.           }
  147.         catch (Exception e)
  148.           {
  149.             throw new RuntimeException(e);
  150.           }
  151.       }

  152.     /***********************************************************************************************************************************************************
  153.      * Create a clone of this object calling the special clone constructor by reflection.
  154.      * @param   override  the override object
  155.      * @return            the clone
  156.      * @deprecated        Use {@link #clonedWith(Object)} instead.
  157.      **********************************************************************************************************************************************************/
  158.     @Nonnull @Deprecated
  159.     protected F clone (@Nonnull final Object override)
  160.       {
  161.         return clonedWith(override);
  162.       }

  163.     /***********************************************************************************************************************************************************
  164.      * {@inheritDoc}
  165.      **********************************************************************************************************************************************************/
  166.     @Override @Nonnull
  167.     public F from (/* @Nonnegative */ final int firstResult)
  168.       {
  169.         return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
  170.       }

  171.     /***********************************************************************************************************************************************************
  172.      * {@inheritDoc}
  173.      **********************************************************************************************************************************************************/
  174.     @Override @Nonnull
  175.     public F max (/* @Nonnegative */ final int maxResults)
  176.       {
  177.         return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
  178.       }

  179.     /***********************************************************************************************************************************************************
  180.      * {@inheritDoc}
  181.      **********************************************************************************************************************************************************/
  182.     @Override @Nonnull
  183.     public F withContext (@Nonnull final Object context)
  184.       {
  185.         final var contexts = concat(this.contexts, context);
  186.         return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
  187.       }

  188.     /***********************************************************************************************************************************************************
  189.      * {@inheritDoc}
  190.      **********************************************************************************************************************************************************/
  191.     @Override @Nonnull
  192.     public <U> Finder<U> ofType (@Nonnull final Class<U> type)
  193.       {
  194.         throw new UnsupportedOperationException("Must be eventually implemented by subclasses.");
  195.       }

  196.     /***********************************************************************************************************************************************************
  197.      * {@inheritDoc}
  198.      **********************************************************************************************************************************************************/
  199.     @Override @Nonnull
  200.     public F sort (@Nonnull final SortCriterion criterion, @Nonnull final SortDirection direction)
  201.       {
  202.         if (criterion instanceof Finder.InMemorySortCriterion)
  203.           {
  204.             @SuppressWarnings("unchecked")
  205.             final var sorters = concat(this.sorters, new Sorter<>((InMemorySortCriterion<T>)criterion, direction));
  206.             return clonedWith(new HierarchicFinderSupport<T, F>(name, firstResult, maxResults, contexts, sorters));
  207.           }

  208.         final var template = "%s does not implement %s - you need to subclass Finder and override sort()";
  209.         final var message = String.format(template, criterion, InMemorySortCriterion.class);
  210.         throw new UnsupportedOperationException(message);
  211.       }

  212.     /***********************************************************************************************************************************************************
  213.      * {@inheritDoc}
  214.      **********************************************************************************************************************************************************/
  215.     @Override @Nonnull
  216.     public final F sort (@Nonnull final SortCriterion criterion)
  217.       {
  218.         return sort(criterion, SortDirection.ASCENDING);
  219.       }

  220.     /***********************************************************************************************************************************************************
  221.      * {@inheritDoc}
  222.      **********************************************************************************************************************************************************/
  223.     @Override @Nonnull
  224.     public List<T> results()
  225.       {
  226.         return computeNeededResults();
  227.       }

  228.     /***********************************************************************************************************************************************************
  229.      * {@inheritDoc}
  230.      **********************************************************************************************************************************************************/
  231.     @Override /* @Nonnegative */
  232.     public int count()
  233.       {
  234.         return computeNeededResults().size();
  235.       }

  236.     /***********************************************************************************************************************************************************
  237.      * Subclasses can implement this method where *all* the raw results must be actually retrieved.
  238.      * @return  the unprocessed results
  239.      **********************************************************************************************************************************************************/
  240.     // START SNIPPET: computeResults
  241.     @Nonnull
  242.     protected List<T> computeResults()
  243.     // END SNIPPET: computeResults
  244.       {
  245.         throw new UnsupportedOperationException("You must implement me!");
  246.       }

  247.     /***********************************************************************************************************************************************************
  248.      * Subclasses can implement this method where *only the requested* raw results must be retrieved.
  249.      * @return  the unprocessed results
  250.      **********************************************************************************************************************************************************/
  251.     // START SNIPPET: computeNeededResults
  252.     @Nonnull
  253.     protected List<T> computeNeededResults()
  254.     // END SNIPPET: computeNeededResults
  255.       {
  256.         log.trace("computeNeededResults() - {}", this);
  257.         var results = computeResults();

  258.         // First sort and then extract the sublist
  259.         for (final var sorter : sorters)
  260.           {
  261.             log.trace(">>>> sorting with {}...", sorter);
  262.             sorter.sort(results);
  263.           }

  264.         final var toIndex = (int)Math.min(results.size(), (long)firstResult + (long)maxResults);

  265.         if (firstResult > toIndex)
  266.           {
  267.             return new CopyOnWriteArrayList<>();
  268.           }

  269.         results = results.subList(firstResult, toIndex);

  270.         return results;
  271.       }

  272.     /***********************************************************************************************************************************************************
  273.      * A utility method used by the copy constructor (see general documentation). If the override object is strictly of the specified type, it is returned;
  274.      * otherwise the other object is returned.
  275.      * @param <U>       the static type of the source
  276.      * @param type      the dynamic type of the source
  277.      * @param other     the other finder
  278.      * @param override  the holder object
  279.      * @return          the override or other
  280.      **********************************************************************************************************************************************************/
  281.     @Nonnull
  282.     protected static <U> U getSource (@Nonnull final Class<? extends U> type,
  283.                                       @Nonnull final U other,
  284.                                       @Nonnull final Object override)
  285.       {
  286.         return override.getClass().equals(type) ? type.cast(override) : other;
  287.       }

  288.     /***********************************************************************************************************************************************************
  289.      *
  290.      **********************************************************************************************************************************************************/
  291.     @Nonnull @SuppressWarnings("unchecked")
  292.     private Constructor<HierarchicFinderSupport<T, F>> getCloneConstructor()
  293.       throws SecurityException, NoSuchMethodException
  294.       {
  295.         return (Constructor<HierarchicFinderSupport<T, F>>)getClass().getConstructor(getClass(), Object.class);
  296.       }

  297.     /***********************************************************************************************************************************************************
  298.      *
  299.      **********************************************************************************************************************************************************/
  300.     private void checkSubClass ()
  301.       {
  302.         try
  303.           {
  304.             getCloneConstructor();
  305.           }
  306.         catch (SecurityException | NoSuchMethodException e)
  307.           {
  308.             throw new ExceptionInInitializerError(MESSAGE + e.getMessage());
  309.           }
  310.       }
  311.   }