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}