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.TimeUnit; 045import java.util.concurrent.TimeoutException; 046import java.util.function.BiConsumer; 047import java.util.function.Consumer; 048 049/** 050 * An UUID pipeline is essentially an ordered list of 051 * {@link UUIDService uuid services} that each get the 052 * opportunity of providing usernames or UUIDs. 053 * <p> 054 * Each request is then passed through a secondary list of 055 * consumers, that can then be used to cache them, etc 056 */ 057public class UUIDPipeline { 058 059 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + UUIDPipeline.class.getSimpleName()); 060 private static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build(); 061 062 private final Executor executor; 063 private final List<UUIDService> serviceList; 064 private final List<Consumer<List<UUIDMapping>>> consumerList; 065 066 /** 067 * Construct a new UUID pipeline 068 * 069 * @param executor Executor that is used to run asynchronous tasks inside 070 * of the pipeline 071 */ 072 public UUIDPipeline(final @NonNull Executor executor) { 073 this.executor = executor; 074 this.serviceList = Lists.newLinkedList(); 075 this.consumerList = Lists.newLinkedList(); 076 } 077 078 /** 079 * Register a UUID service 080 * 081 * @param uuidService UUID service to register 082 */ 083 public void registerService(final @NonNull UUIDService uuidService) { 084 this.serviceList.add(uuidService); 085 } 086 087 /** 088 * Register a mapping consumer 089 * 090 * @param mappingConsumer Consumer to register 091 */ 092 public void registerConsumer(final @NonNull Consumer<@NonNull List<@NonNull UUIDMapping>> mappingConsumer) { 093 this.consumerList.add(mappingConsumer); 094 } 095 096 /** 097 * Get a copy of the service list 098 * 099 * @return Copy of service list 100 */ 101 public @NonNull List<@NonNull UUIDService> getServiceListInstance() { 102 return Collections.unmodifiableList(this.serviceList); 103 } 104 105 /** 106 * Let all consumers act on the given mapping. 107 * 108 * @param mappings Mappings 109 */ 110 public void consume(final @NonNull List<@NonNull UUIDMapping> mappings) { 111 final Runnable runnable = () -> { 112 for (final Consumer<List<UUIDMapping>> consumer : this.consumerList) { 113 consumer.accept(mappings); 114 } 115 }; 116 if (PlotSquared.get().isMainThread(Thread.currentThread())) { 117 TaskManager.runTaskAsync(runnable); 118 } else { 119 runnable.run(); 120 } 121 } 122 123 /** 124 * Consume a single mapping 125 * 126 * @param mapping Mapping to consume 127 */ 128 public void consume(final @NonNull UUIDMapping mapping) { 129 this.consume(Collections.singletonList(mapping)); 130 } 131 132 /** 133 * This will store the given username-UUID pair directly, and overwrite 134 * any existing caches. This can be used to update usernames automatically 135 * whenever a player joins the server, to make sure an up-to-date UUID 136 * mapping is stored 137 * 138 * @param username Player username 139 * @param uuid Player uuid 140 */ 141 public void storeImmediately(final @NonNull String username, final @NonNull UUID uuid) { 142 this.consume(new UUIDMapping(uuid, username)); 143 } 144 145 /** 146 * Get a single UUID from a username. This is blocking. 147 * 148 * @param username Username 149 * @param timeout Timeout in milliseconds 150 * @return The mapped uuid. Will return null if the request timed out. 151 */ 152 public @Nullable UUID getSingle(final @NonNull String username, final long timeout) { 153 ThreadUtils.catchSync("Blocking UUID retrieval from the main thread"); 154 try { 155 final List<UUIDMapping> mappings = this.getUUIDs(Collections.singletonList(username)).get( 156 timeout, 157 TimeUnit.MILLISECONDS 158 ); 159 if (mappings.size() == 1) { 160 return mappings.get(0).uuid(); 161 } 162 } catch (InterruptedException | ExecutionException e) { 163 e.printStackTrace(); 164 } catch (TimeoutException ignored) { 165 // This is completely valid, we just don't care anymore 166 if (Settings.DEBUG) { 167 LOGGER.warn("(UUID) Request for {} timed out. Rate limit.", username); 168 } 169 } 170 return null; 171 } 172 173 /** 174 * Get a single username from a UUID. This is blocking. 175 * 176 * @param uuid UUID 177 * @param timeout Timeout in milliseconds 178 * @return The mapped username. Will return null if the request timeout. 179 */ 180 public @Nullable String getSingle(final @NonNull UUID uuid, final long timeout) { 181 ThreadUtils.catchSync("Blocking username retrieval from the main thread"); 182 try { 183 final List<UUIDMapping> mappings = this.getNames(Collections.singletonList(uuid)).get(timeout, TimeUnit.MILLISECONDS); 184 if (mappings.size() == 1) { 185 return mappings.get(0).username(); 186 } 187 } catch (InterruptedException | ExecutionException e) { 188 e.printStackTrace(); 189 } catch (TimeoutException ignored) { 190 // This is completely valid, we just don't care anymore 191 if (Settings.DEBUG) { 192 LOGGER.warn("(UUID) Request for {} timed out. Rate limit.", uuid); 193 } 194 } 195 return null; 196 } 197 198 /** 199 * Get a single UUID from a username. This is non-blocking. 200 * 201 * @param username Username 202 * @param uuid UUID consumer 203 */ 204 public void getSingle(final @NonNull String username, final @NonNull BiConsumer<@Nullable UUID, @Nullable Throwable> uuid) { 205 this.getUUIDs(Collections.singletonList(username)) 206 .orTimeout(Settings.UUID.NON_BLOCKING_TIMEOUT, TimeUnit.MILLISECONDS) 207 .whenComplete((uuids, throwable) -> { 208 if (throwable != null) { 209 uuid.accept(null, throwable); 210 } else { 211 if (!uuids.isEmpty()) { 212 uuid.accept(uuids.get(0).uuid(), null); 213 } else { 214 uuid.accept(null, null); 215 } 216 } 217 }); 218 } 219 220 /** 221 * Get a single username from a UUID. This is non-blocking. 222 * 223 * @param uuid UUID 224 * @param username Username consumer 225 */ 226 public void getSingle(final @NonNull UUID uuid, final @NonNull BiConsumer<@Nullable String, @Nullable Throwable> username) { 227 this.getNames(Collections.singletonList(uuid)) 228 .orTimeout(Settings.UUID.NON_BLOCKING_TIMEOUT, TimeUnit.MILLISECONDS) 229 .whenComplete((uuids, throwable) -> { 230 if (throwable != null) { 231 username.accept(null, throwable); 232 } else { 233 if (!uuids.isEmpty()) { 234 username.accept(uuids.get(0).username(), null); 235 } else { 236 username.accept(null, null); 237 } 238 } 239 }); 240 } 241 242 /** 243 * Asynchronously attempt to fetch the mapping from a list of UUIDs. 244 * <p> 245 * This will timeout after the specified time and throws a {@link TimeoutException} 246 * if this happens 247 * 248 * @param requests UUIDs 249 * @param timeout Timeout in milliseconds 250 * @return Mappings 251 */ 252 public @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> getNames( 253 final @NonNull Collection<@NonNull UUID> requests, 254 final long timeout 255 ) { 256 return this.getNames(requests).orTimeout(timeout, TimeUnit.MILLISECONDS); 257 } 258 259 /** 260 * Asynchronously attempt to fetch the mapping from a list of names. 261 * <p> 262 * This will timeout after the specified time and throws a {@link TimeoutException} 263 * if this happens 264 * 265 * @param requests Names 266 * @param timeout Timeout in milliseconds 267 * @return Mappings 268 */ 269 public @NonNull CompletableFuture<List<UUIDMapping>> getUUIDs( 270 final @NonNull Collection<String> requests, 271 final long timeout 272 ) { 273 return this.getUUIDs(requests).orTimeout(timeout, TimeUnit.MILLISECONDS); 274 } 275 276 /** 277 * Asynchronously attempt to fetch the mapping from a list of UUIDs 278 * 279 * @param requests UUIDs 280 * @return Mappings 281 */ 282 public @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> getNames( 283 final @NonNull Collection<@NonNull UUID> requests 284 ) { 285 if (requests.isEmpty()) { 286 return CompletableFuture.completedFuture(Collections.emptyList()); 287 } 288 289 final List<UUIDService> serviceList = this.getServiceListInstance(); 290 final List<UUIDMapping> mappings = new ArrayList<>(requests.size()); 291 final List<UUID> remainingRequests = new ArrayList<>(requests); 292 293 for (final UUIDService service : serviceList) { 294 // We can chain multiple synchronous 295 // ones in a row 296 if (service.canBeSynchronous()) { 297 final List<UUIDMapping> completedRequests = service.getNames(remainingRequests); 298 for (final UUIDMapping mapping : completedRequests) { 299 remainingRequests.remove(mapping.uuid()); 300 } 301 mappings.addAll(completedRequests); 302 } else { 303 break; 304 } 305 if (remainingRequests.isEmpty()) { 306 return CompletableFuture.completedFuture(mappings); 307 } 308 } 309 310 return CompletableFuture.supplyAsync(() -> { 311 for (final UUIDService service : serviceList) { 312 final List<UUIDMapping> completedRequests = service.getNames(remainingRequests); 313 for (final UUIDMapping mapping : completedRequests) { 314 remainingRequests.remove(mapping.uuid()); 315 } 316 mappings.addAll(completedRequests); 317 if (remainingRequests.isEmpty()) { 318 break; 319 } 320 } 321 322 if (mappings.size() == requests.size()) { 323 this.consume(mappings); 324 return mappings; 325 } else if (Settings.DEBUG) { 326 LOGGER.info("(UUID) Failed to find all usernames"); 327 } 328 329 if (Settings.UUID.UNKNOWN_AS_DEFAULT) { 330 for (final UUID uuid : remainingRequests) { 331 mappings.add(new UUIDMapping( 332 uuid, 333 MINI_MESSAGE.escapeTags(TranslatableCaption 334 .of("info.unknown") 335 .getComponent(ConsolePlayer.getConsole())) 336 )); 337 } 338 return mappings; 339 } else { 340 throw new ServiceError("End of pipeline"); 341 } 342 }, this.executor); 343 } 344 345 /** 346 * Asynchronously attempt to fetch the mapping from a list of names 347 * 348 * @param requests Names 349 * @return Mappings 350 */ 351 public @NonNull CompletableFuture<@NonNull List<@NonNull UUIDMapping>> getUUIDs( 352 final @NonNull Collection<@NonNull String> requests 353 ) { 354 if (requests.isEmpty()) { 355 return CompletableFuture.completedFuture(Collections.emptyList()); 356 } 357 358 final List<UUIDService> serviceList = this.getServiceListInstance(); 359 final List<UUIDMapping> mappings = new ArrayList<>(requests.size()); 360 final List<String> remainingRequests = new ArrayList<>(requests); 361 362 for (final UUIDService service : serviceList) { 363 // We can chain multiple synchronous 364 // ones in a row 365 if (service.canBeSynchronous()) { 366 final List<UUIDMapping> completedRequests = service.getUUIDs(remainingRequests); 367 for (final UUIDMapping mapping : completedRequests) { 368 remainingRequests.remove(mapping.username()); 369 } 370 mappings.addAll(completedRequests); 371 } else { 372 break; 373 } 374 if (remainingRequests.isEmpty()) { 375 return CompletableFuture.completedFuture(mappings); 376 } 377 } 378 379 return CompletableFuture.supplyAsync(() -> { 380 for (final UUIDService service : serviceList) { 381 final List<UUIDMapping> completedRequests = service.getUUIDs(remainingRequests); 382 for (final UUIDMapping mapping : completedRequests) { 383 remainingRequests.remove(mapping.username()); 384 } 385 mappings.addAll(completedRequests); 386 if (remainingRequests.isEmpty()) { 387 break; 388 } 389 } 390 391 if (mappings.size() == requests.size()) { 392 this.consume(mappings); 393 return mappings; 394 } else if (Settings.DEBUG) { 395 LOGGER.info("(UUID) Failed to find all UUIDs"); 396 } 397 398 throw new ServiceError("End of pipeline"); 399 }, this.executor); 400 } 401 402 /** 403 * Get as many UUID mappings as possible under the condition 404 * that the operation cannot be blocking (for an extended amount of time) 405 * 406 * @return All mappings that could be provided immediately 407 */ 408 public @NonNull 409 final Collection<@NonNull UUIDMapping> getAllImmediately() { 410 final Set<UUIDMapping> mappings = new LinkedHashSet<>(); 411 for (final UUIDService service : this.getServiceListInstance()) { 412 mappings.addAll(service.getImmediately()); 413 } 414 return mappings; 415 } 416 417 /** 418 * Get a single UUID mapping immediately, if possible 419 * 420 * @param object Username ({@link String}) or {@link UUID} 421 * @return Mapping, if it could be found immediately 422 */ 423 public @Nullable 424 final UUIDMapping getImmediately(final @NonNull Object object) { 425 for (final UUIDService uuidService : this.getServiceListInstance()) { 426 final UUIDMapping mapping = uuidService.getImmediately(object); 427 if (mapping != null) { 428 return mapping; 429 } 430 } 431 return null; 432 } 433 434}