diff --git a/src/main/java/org/spongepowered/api/profile/GameProfileCache.java b/src/main/java/org/spongepowered/api/profile/GameProfileCache.java new file mode 100644 index 00000000000..7e7b5697e3f --- /dev/null +++ b/src/main/java/org/spongepowered/api/profile/GameProfileCache.java @@ -0,0 +1,314 @@ +/* + * This file is part of SpongeAPI, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.api.profile; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.spongepowered.api.entity.living.player.User; +import org.spongepowered.api.service.user.UserStorageService; +import org.spongepowered.api.util.GuavaCollectors; + +import java.util.Collection; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.annotation.Nullable; + +/** + * Represents a cache of {@link GameProfile}s. + */ +public interface GameProfileCache { + + /** + * Add an entry to this cache. + * + * @param profile The profile to cache + * @return {@code true} if the profile was successfully cached, + * otherwise {@code false} + */ + default boolean add(GameProfile profile) { + return this.add(profile, null); + } + + /** + * Add an entry to this cache, with an optional expiration date. + * + * @param profile The profile to cache + * @param expiry The expiration date + * @return {@code true} if the profile was successfully cached, + * otherwise {@code false} + */ + default boolean add(GameProfile profile, @Nullable Date expiry) { + return this.add(profile, false, expiry); + } + + /** + * Add an entry to this cache, with an optional expiration date. + * + * @param profile The profile to cache + * @param overwrite If we should overwrite the cache entry for + * the provided profile + * @param expiry The expiration date + * @return {@code true} if the profile was successfully cached, + * otherwise {@code false} + */ + boolean add(GameProfile profile, boolean overwrite, @Nullable Date expiry); + + /** + * Gets a {@link GameProfile} from this cache by its unique id. + * + * @param uniqueId The unique id of the profile + * @return The profile, if present, or {@link Optional#empty()} if + * the cache did not contain a profile with the provided id + */ + Optional getById(UUID uniqueId); + + /** + * Gets {@link GameProfile}s in bulk by their unique id. + * + * @param uniqueIds The unique ids + * @return A collection of successfully found up profiles + */ + default Map> getByIds(Iterable uniqueIds) { + checkNotNull(uniqueIds, "unique ids"); + + Map> result = Maps.newHashMap(); + uniqueIds.forEach(uniqueId -> result.put(uniqueId, this.getById(uniqueId))); + + return ImmutableMap.copyOf(result); + } + + /** + * Looks a {@link GameProfile} up by its unique id and + * loads it into this cache. + * + * @param uniqueId The unique id of the profile + * @return The profile, if present, or {@link Optional#empty()} if + * we couldn't find a profile with the provided id + */ + Optional lookupById(UUID uniqueId); + + /** + * Looks up {@link GameProfile}s in bulk by their unique id and + * loads them into this cache. + * + * @param uniqueIds The unique ids + * @return A collection of successfully looked up profiles + */ + default Map> lookupByIds(Iterable uniqueIds) { + checkNotNull(uniqueIds, "unique ids"); + + Map> result = Maps.newHashMap(); + uniqueIds.forEach(uniqueId -> result.put(uniqueId, this.lookupById(uniqueId))); + + return ImmutableMap.copyOf(result); + } + + /** + * Gets a {@link GameProfile} from this cache by its id if available, + * or lookups the profile by its unique id. + * + * @param uniqueId The unique id of the profile + * @return The profile, if present, or {@link Optional#empty()} if + * the cache did not contain a profile with the provided id and + * we couldn't lookup a profile with the provided id + */ + default Optional getOrLookupById(UUID uniqueId) { + Optional profile = this.getById(uniqueId); + if (profile.isPresent()) { + return profile; + } else { + return this.lookupById(uniqueId); + } + } + + /** + * Gets {@link GameProfile}s in bulk from this cache by when available, + * and lookups the profiles by their unique id when not cached. + * + * @param uniqueIds The unique ids of the profiles + * @return A collection of successfully found profiles + */ + default Map> getOrLookupByIds(Iterable uniqueIds) { + checkNotNull(uniqueIds, "unique ids"); + + Collection pending = Sets.newHashSet(uniqueIds); + Map> result = Maps.newHashMap(); + + result.putAll(this.getByIds(pending)); + result.forEach((uniqueId, profile) -> pending.remove(uniqueId)); + result.putAll(this.lookupByIds(pending)); + + return ImmutableMap.copyOf(result); + } + + /** + * Gets a {@link GameProfile} from this cache by its name. + * + * @param name The name of the profile + * @return The profile, if present, or {@link Optional#empty()} if + * the cache did not contain a profile with the provided name + */ + Optional getByName(String name); + + /** + * Gets {@link GameProfile}s in bulk by their name. + * + * @param names The names + * @return A collection of successfully found up profiles + */ + default Map> getByNames(Iterable names) { + checkNotNull(names, "names"); + + Map> result = Maps.newHashMap(); + names.forEach(name -> result.put(name, this.getByName(name))); + + return ImmutableMap.copyOf(result); + } + + /** + * Looks a {@link GameProfile} up by its name and + * loads it into this cache. + * + * @param name The name of the profile + * @return The profile, if present, or {@link Optional#empty()} if + * we couldn't find a profile with the provided name + */ + Optional lookupByName(String name); + + /** + * Looks up {@link GameProfile}s in bulk by their name and + * loads them into this cache. + * + * @param names The names + * @return A collection of successfully looked up profiles + */ + default Map> lookupByNames(Iterable names) { + checkNotNull(names, "names"); + + Map> result = Maps.newHashMap(); + names.forEach(name -> result.put(name, this.lookupByName(name))); + + return ImmutableMap.copyOf(result); + } + + /** + * Gets a {@link GameProfile} from this cache by its if available, + * or lookups the profile by its name. + * + * @param name The name of the profile + * @return The profile, if present, or {@link Optional#empty()} if + * the cache did not contain a profile with the provided name and + * we couldn't lookup a profile with the provided name + */ + default Optional getOrLookupByName(String name) { + Optional profile = this.getByName(name); + if (profile.isPresent()) { + return profile; + } else { + return this.lookupByName(name); + } + } + + /** + * Gets {@link GameProfile}s in bulk from this cache by when available, + * and lookups the profiles by their unique id when not cached. + * + * @param names The names of the profiles + * @return A collection of successfully found profiles + */ + default Map> getOrLookupByNames(Iterable names) { + checkNotNull(names, "names"); + + Collection pending = Sets.newHashSet(names); + Map> result = Maps.newHashMap(); + + result.putAll(this.getByNames(pending)); + result.forEach((name, profile) -> pending.remove(name)); + result.putAll(this.lookupByNames(pending)); + + return ImmutableMap.copyOf(result); + } + + /** + * Fills a {@link GameProfile} from cached values. + * + * @param profile The profile to fill + * @return The filled profile, if present, or {@link Optional#empty()} if + * we were unable to fill the profile + */ + default Optional fillProfile(GameProfile profile) { + return this.fillProfile(profile, false); + } + + /** + * Fills a {@link GameProfile} from cached values. + * + * @param profile The profile to fill + * @param signed true if we should request that the profile data be signed + * @return The filled profile, if present, or {@link Optional#empty()} if + * we were unable to fill the profile + */ + Optional fillProfile(GameProfile profile, boolean signed); + + /** + * Gets a collection of all cached {@link GameProfile}s. + * + * @return A {@link Collection} of cached {@link GameProfile}s + */ + Collection getProfiles(); + + /** + * Returns a collection of matching cached {@link GameProfile}s whose last + * known names start with the given string (case-insensitive). + * + *

This collection may also contain profiles of players who never played + * on the server!

+ * + *

Use {@link UserStorageService#match(String)} for a collection that + * only contains {@link GameProfile}s with attached {@link User} data.

+ * + *

This method only searches the local cache, so the data may not be up + * to date.

+ * + * @param name The name + * @return A {@link Collection} of matching {@link GameProfile}s + */ + default Collection match(String name) { + final String search = checkNotNull(name, "name").toLowerCase(Locale.ROOT); + + return this.getProfiles().stream() + .filter(profile -> profile.getName().isPresent()) + .filter(profile -> profile.getName().get().toLowerCase(Locale.ROOT).startsWith(search)) + .collect(GuavaCollectors.toImmutableSet()); + } + +} diff --git a/src/main/java/org/spongepowered/api/profile/GameProfileManager.java b/src/main/java/org/spongepowered/api/profile/GameProfileManager.java index 9e49a841c88..aa84765e19a 100644 --- a/src/main/java/org/spongepowered/api/profile/GameProfileManager.java +++ b/src/main/java/org/spongepowered/api/profile/GameProfileManager.java @@ -199,31 +199,60 @@ default CompletableFuture fill(GameProfile profile, boolean signed) */ CompletableFuture fill(GameProfile profile, boolean signed, boolean useCache); + /** + * Gets the active {@link GameProfile} cache. + * + * @return The active cache + */ + GameProfileCache getCache(); + + /** + * Sets the {@link GameProfile} cache. + * + *

To restore the original cache, pass the result of {@link #getDefaultCache()}.

+ * + * @param cache The new cache + */ + void setCache(GameProfileCache cache); + + /** + * Gets the default cache. + * + * @return The default cache. + */ + GameProfileCache getDefaultCache(); + /** * Gets a collection of all cached {@link GameProfile}s. * * @return A {@link Collection} of {@link GameProfile}s + * @deprecated use {@link GameProfileCache#getProfiles()} instead */ - Collection getCachedProfiles(); + @Deprecated + default Collection getCachedProfiles() { + return this.getCache().getProfiles(); + } /** * Returns a collection of matching cached {@link GameProfile}s whose last - * known user names start with the given string (case-insensitive). + * known names start with the given string (case-insensitive). * *

This collection may also contain profiles of players who never played * on the server!

* - *

Use - * {@link UserStorageService#match(String)} for - * a collection that only contains {@link GameProfile}s with attached - * {@link User} data.

+ *

Use {@link UserStorageService#match(String)} for a collection that + * only contains {@link GameProfile}s with attached {@link User} data.

* *

This method only searches the local cache, so the data may not be up * to date.

* - * @param lastKnownName The user name - * @return The result of the request + * @param name The name + * @return A {@link Collection} of matching {@link GameProfile}s + * @deprecated use {@link GameProfileCache#match(String)} instead */ - Collection match(String lastKnownName); + @Deprecated + default Collection match(String name) { + return this.getCache().match(name); + } } diff --git a/src/main/java/org/spongepowered/api/profile/package-info.java b/src/main/java/org/spongepowered/api/profile/package-info.java index b24058e7b62..b81dca01529 100644 --- a/src/main/java/org/spongepowered/api/profile/package-info.java +++ b/src/main/java/org/spongepowered/api/profile/package-info.java @@ -22,4 +22,5 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -@org.spongepowered.api.util.annotation.NonnullByDefault package org.spongepowered.api.profile; +@org.spongepowered.api.util.annotation.NonnullByDefault +package org.spongepowered.api.profile;