001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.uuid; 020 021import com.google.common.collect.Lists; 022import com.plotsquared.core.PlotSquared; 023import com.plotsquared.core.configuration.Settings; 024import com.plotsquared.core.configuration.caption.TranslatableCaption; 025import com.plotsquared.core.player.ConsolePlayer; 026import com.plotsquared.core.util.ThreadUtils; 027import com.plotsquared.core.util.task.TaskManager; 028import net.kyori.adventure.text.minimessage.MiniMessage; 029import org.apache.logging.log4j.LogManager; 030import org.apache.logging.log4j.Logger; 031import org.checkerframework.checker.nullness.qual.NonNull; 032import org.checkerframework.checker.nullness.qual.Nullable; 033 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.Collections; 037import java.util.LinkedHashSet; 038import java.util.List; 039import java.util.Set; 040import java.util.UUID; 041import java.util.concurrent.CompletableFuture; 042import java.util.concurrent.ExecutionException; 043import java.util.concurrent.Executor; 044import java.util.concurrent.Executors; 045import java.util.concurrent.ScheduledExecutorService; 046import java.util.concurrent.TimeUnit; 047import java.util.concurrent.TimeoutException; 048import java.util.function.BiConsumer; 049import java.util.function.Consumer; 050import java.util.function.Function; 051 052/** 053 * An UUID pipeline is essentially an ordered list of 054 * {@link UUIDService uuid services} that each get the 055 * opportunity of providing usernames or UUIDs. 056 * <p> 057 * Each request is then passed through a secondary list of 058 * consumers, that can then be used to cache them, etc 059 */ 060public class UUIDPipeline { 061 062 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + UUIDPipeline.class.getSimpleName()); 063 private static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build(); 064 065 private final Executor executor; 066 private final List<UUIDService> serviceList; 067 private final List<Consumer<List<UUIDMapping>>> consumerList; 068 private final ScheduledExecutorService timeoutExecutor; 069 070 /** 071 * Construct a new UUID pipeline 072 * 073 * @param executor Executor that is used to run asynchronous tasks inside 074 * of the pipeline 075 */ 076 public UUIDPipeline(final @NonNull Executor executor) { 077 this.executor = executor; 078 this.serviceList = Lists.newLinkedList(); 079 this.consumerList = Lists.newLinkedList(); 080 this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor(); 081 } 082 083 /** 084 * Register a UUID service 085 * 086 * @param uuidService UUID service to register 087 */ 088 public void registerService(final @NonNull UUIDService uuidService) { 089 this.serviceList.add(uuidService); 090 } 091 092 /** 093 * Register a mapping consumer 094 * 095 * @param mappingConsumer Consumer to register 096 */ 097 public void registerConsumer(final @NonNull Consumer<@NonNull List<@NonNull UUIDMapping>> mappingConsumer) { 098 this.consumerList.add(mappingConsumer); 099 } 100 101 /** 102 * Get a copy of the service list 103 * 104 * @return Copy of service list 105 */ 106 public @NonNull List<@NonNull UUIDService> getServiceListInstance() { 107 return Collections.unmodifiableList(this.serviceList); 108 } 109 110 /** 111 * Let all consumers act on the given mapping. 112 * 113 * @param mappings Mappings 114 */ 115 public void consume(final @NonNull List<@NonNull UUIDMapping> mappings) { 116 final Runnable runnable = () -> { 117 for (final Consumer<List<UUIDMapping>> consumer : this.consumerList) { 118 consumer.accept(mappings); 119 } 120 }; 121 if (PlotSquared.get().isMainThread(Thread.currentThread())) { 122 TaskManager.runTaskAsync(runnable); 123 } else { 124 runnable.run(); 125 } 126 } 127 128 /** 129 * Consume a single mapping 130 * 131 * @param mapping Mapping to consume 132 */ 133 public void consume(final @NonNull UUIDMapping mapping) { 134 this.consume(Collections.singletonList(mapping)); 135 } 136 137 /** 138 * This will store the given username-UUID pair directly, and overwrite 139 * any existing caches. This can be used to update usernames automatically 140 * whenever a player joins the server, to make sure an up-to-date UUID 141 * mapping is stored 142 * 143 * @param username Player username 144 * @param uuid Player uuid 145 */ 146 public void storeImmediately(final @NonNull String username, final @NonNull UUID uuid) { 147 this.consume(new UUIDMapping(uuid, username)); 148 } 149 150 /** 151 * Get a single UUID from a username. This is blocking. 152 * 153 * @param username Username 154 * @param timeout Timeout in milliseconds 155 * @return The mapped uuid. Will return null if the request timed out. 156 */ 157 public @Nullable UUID getSingle(final @NonNull String username, final long timeout) { 158 ThreadUtils.catchSync("Blocking UUID retrieval from the main thread"); 159 try { 160 final List<UUIDMapping> mappings = this.getUUIDs(Collections.singletonList(username)).get( 161 timeout, 162 TimeUnit.MILLISECONDS 163 ); 164 if (mappings.size() == 1) { 165 return mappings.get(0).getUuid(); 166 } 167 } catch (InterruptedException | ExecutionException e) { 168 e.printStackTrace(); 169 } catch (TimeoutException ignored) { 170 // This is completely valid, we just don't care anymore 171 if (Settings.DEBUG) { 172 LOGGER.warn("(UUID) Request for {} timed out. Rate limit.", username); 173 } 174 } 175 return null; 176 } 177 178 /** 179 * Get a single username from a UUID. This is blocking. 180 * 181 * @param uuid UUID 182 * @param timeout Timeout in milliseconds 183 * @return The mapped username. Will return null if the request timeout. 184 */ 185 public @Nullable String getSingle(final @NonNull UUID uuid, final long timeout) { 186 ThreadUtils.catchSync("Blocking username retrieval from the main thread"); 187 try { 188 final List<UUIDMapping> mappings = this.getNames(Collections.singletonList(uuid)).get(timeout, TimeUnit.MILLISECONDS); 189 if (mappings.size() == 1) { 190 return mappings.get(0).getUsername(); 191 } 192 } catch (InterruptedException | ExecutionException e) { 193 e.printStackTrace(); 194 } catch (TimeoutException ignored) { 195 // This is completely valid, we just don't care anymore 196 if (Settings.DEBUG) { 197 LOGGER.warn("(UUID) Request for {} timed out. Rate limit.", uuid); 198 } 199 } 200 return null; 201 } 202 203 /** 204 * Get a single UUID from a username. This is non-blocking. 205 * 206 * @param username Username 207 * @param uuid UUID consumer 208 */ 209 public void getSingle(final @NonNull String username, final @NonNull BiConsumer<@Nullable UUID, @Nullable Throwable> uuid) { 210 this.getUUIDs(Collections.singletonList(username)).applyToEither( 211 timeoutAfter(Settings.UUID.NON_BLOCKING_TIMEOUT), 212 Function.identity() 213 ) 214 .whenComplete((uuids, throwable) -> { 215 if (throwable != null) { 216 uuid.accept(null, throwable); 217 } else { 218 if (!uuids.isEmpty()) { 219 uuid.accept(uuids.get(0).getUuid(), null); 220 } else { 221 uuid.accept(null, null); 222 } 223 } 224 }); 225 } 226 227 /** 228 * Get a single username from a UUID. This is non-blocking. 229 * 230 * @param uuid UUID 231 * @param username Username consumer 232 */ 233 public void getSingle(final @NonNull UUID uuid, final @NonNull BiConsumer<@Nullable String, @Nullable Throwable> username) { 234 this.getNames(Collections.singletonList(uuid)).applyToEither( 235 timeoutAfter(Settings.UUID.NON_BLOCKING_TIMEOUT), 236 Function.identity() 237 ) 238 .whenComplete((uuids, throwable) -> { 239 if (throwable != null) { 240 username.accept(null, throwable); 241 } else { 242 if (!uuids.isEmpty()) { 243 username.accept(uuids.get(0).getUsername(), null); 244 } else { 245 username.accept(null, null); 246 } 247 } 248 }); 249 } 250 251 /** 252 * Asynchronously attempt to fetch the mapping from a list of UUIDs. 253 * <p> 254 * This will timeout after the specified time and throws a {@link TimeoutException} 255 * if this happens 256 * 257 * @param requests UUIDs 258 * @param timeout Timeout in milliseconds 259 * @return Mappings 260 */ 261 public @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> getNames( 262 final @NonNull Collection<@NonNull UUID> requests, 263 final long timeout 264 ) { 265 return this.getNames(requests).applyToEither(timeoutAfter(timeout), Function.identity()); 266 } 267 268 /** 269 * Asynchronously attempt to fetch the mapping from a list of names. 270 * <p> 271 * This will timeout after the specified time and throws a {@link TimeoutException} 272 * if this happens 273 * 274 * @param requests Names 275 * @param timeout Timeout in milliseconds 276 * @return Mappings 277 */ 278 public @NonNull CompletableFuture<List<UUIDMapping>> getUUIDs( 279 final @NonNull Collection<String> requests, 280 final long timeout 281 ) { 282 return this.getUUIDs(requests).applyToEither(timeoutAfter(timeout), Function.identity()); 283 } 284 285 private @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> timeoutAfter(final long timeout) { 286 final CompletableFuture<List<UUIDMapping>> result = new CompletableFuture<>(); 287 this.timeoutExecutor.schedule(() -> result.completeExceptionally(new TimeoutException()), timeout, TimeUnit.MILLISECONDS); 288 return result; 289 } 290 291 /** 292 * Asynchronously attempt to fetch the mapping from a list of UUIDs 293 * 294 * @param requests UUIDs 295 * @return Mappings 296 */ 297 public @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> getNames( 298 final @NonNull Collection<@NonNull UUID> requests 299 ) { 300 if (requests.isEmpty()) { 301 return CompletableFuture.completedFuture(Collections.emptyList()); 302 } 303 304 final List<UUIDService> serviceList = this.getServiceListInstance(); 305 final List<UUIDMapping> mappings = new ArrayList<>(requests.size()); 306 final List<UUID> remainingRequests = new ArrayList<>(requests); 307 308 for (final UUIDService service : serviceList) { 309 // We can chain multiple synchronous 310 // ones in a row 311 if (service.canBeSynchronous()) { 312 final List<UUIDMapping> completedRequests = service.getNames(remainingRequests); 313 for (final UUIDMapping mapping : completedRequests) { 314 remainingRequests.remove(mapping.getUuid()); 315 } 316 mappings.addAll(completedRequests); 317 } else { 318 break; 319 } 320 if (remainingRequests.isEmpty()) { 321 return CompletableFuture.completedFuture(mappings); 322 } 323 } 324 325 return CompletableFuture.supplyAsync(() -> { 326 for (final UUIDService service : serviceList) { 327 final List<UUIDMapping> completedRequests = service.getNames(remainingRequests); 328 for (final UUIDMapping mapping : completedRequests) { 329 remainingRequests.remove(mapping.getUuid()); 330 } 331 mappings.addAll(completedRequests); 332 if (remainingRequests.isEmpty()) { 333 break; 334 } 335 } 336 337 if (mappings.size() == requests.size()) { 338 this.consume(mappings); 339 return mappings; 340 } else if (Settings.DEBUG) { 341 LOGGER.info("(UUID) Failed to find all usernames"); 342 } 343 344 if (Settings.UUID.UNKNOWN_AS_DEFAULT) { 345 for (final UUID uuid : remainingRequests) { 346 mappings.add(new UUIDMapping( 347 uuid, 348 MINI_MESSAGE.stripTokens(TranslatableCaption 349 .of("info.unknown") 350 .getComponent(ConsolePlayer.getConsole())) 351 )); 352 } 353 return mappings; 354 } else { 355 throw new ServiceError("End of pipeline"); 356 } 357 }, this.executor); 358 } 359 360 /** 361 * Asynchronously attempt to fetch the mapping from a list of names 362 * 363 * @param requests Names 364 * @return Mappings 365 */ 366 public @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> getUUIDs( 367 final @NonNull Collection<@NonNull String> requests 368 ) { 369 if (requests.isEmpty()) { 370 return CompletableFuture.completedFuture(Collections.emptyList()); 371 } 372 373 final List<UUIDService> serviceList = this.getServiceListInstance(); 374 final List<UUIDMapping> mappings = new ArrayList<>(requests.size()); 375 final List<String> remainingRequests = new ArrayList<>(requests); 376 377 for (final UUIDService service : serviceList) { 378 // We can chain multiple synchronous 379 // ones in a row 380 if (service.canBeSynchronous()) { 381 final List<UUIDMapping> completedRequests = service.getUUIDs(remainingRequests); 382 for (final UUIDMapping mapping : completedRequests) { 383 remainingRequests.remove(mapping.getUsername()); 384 } 385 mappings.addAll(completedRequests); 386 } else { 387 break; 388 } 389 if (remainingRequests.isEmpty()) { 390 return CompletableFuture.completedFuture(mappings); 391 } 392 } 393 394 return CompletableFuture.supplyAsync(() -> { 395 for (final UUIDService service : serviceList) { 396 final List<UUIDMapping> completedRequests = service.getUUIDs(remainingRequests); 397 for (final UUIDMapping mapping : completedRequests) { 398 remainingRequests.remove(mapping.getUsername()); 399 } 400 mappings.addAll(completedRequests); 401 if (remainingRequests.isEmpty()) { 402 break; 403 } 404 } 405 406 if (mappings.size() == requests.size()) { 407 this.consume(mappings); 408 return mappings; 409 } else if (Settings.DEBUG) { 410 LOGGER.info("(UUID) Failed to find all UUIDs"); 411 } 412 413 throw new ServiceError("End of pipeline"); 414 }, this.executor); 415 } 416 417 /** 418 * Get as many UUID mappings as possible under the condition 419 * that the operation cannot be blocking (for an extended amount of time) 420 * 421 * @return All mappings that could be provided immediately 422 */ 423 public @NonNull 424 final Collection<@NonNull UUIDMapping> getAllImmediately() { 425 final Set<UUIDMapping> mappings = new LinkedHashSet<>(); 426 for (final UUIDService service : this.getServiceListInstance()) { 427 mappings.addAll(service.getImmediately()); 428 } 429 return mappings; 430 } 431 432 /** 433 * Get a single UUID mapping immediately, if possible 434 * 435 * @param object Username ({@link String}) or {@link UUID} 436 * @return Mapping, if it could be found immediately 437 */ 438 public @Nullable 439 final UUIDMapping getImmediately(final @NonNull Object object) { 440 for (final UUIDService uuidService : this.getServiceListInstance()) { 441 final UUIDMapping mapping = uuidService.getImmediately(object); 442 if (mapping != null) { 443 return mapping; 444 } 445 } 446 return null; 447 } 448 449}