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}