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 }