/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * 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.lanternpowered.server.profile; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import ninja.leaping.configurate.objectmapping.Setting; import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; import org.lanternpowered.server.config.ConfigBase; import org.lanternpowered.server.game.Lantern; import org.spongepowered.api.profile.GameProfile; import org.spongepowered.api.profile.GameProfileCache; import org.spongepowered.api.profile.ProfileNotFoundException; import org.spongepowered.api.util.GuavaCollectors; import java.io.IOException; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import javax.annotation.Nullable; public final class LanternGameProfileCache implements GameProfileCache { // The duration before a profile expires private static final Duration EXPIRATION_DURATION = Duration.ofDays(30); // Lookup by name private final Map<String, ProfileCacheEntry> byName = new ConcurrentHashMap<>(); // Lookup by unique id private final Map<UUID, ProfileCacheEntry> byUUID = new ConcurrentHashMap<>(); // The cache file private final ProfileCacheFile cacheFile; LanternGameProfileCache(Path cacheFile) { ProfileCacheFile cache = null; try { cache = new ProfileCacheFile(cacheFile); try { cache.load(); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while loading the profile cache file.", e); } } catch (IOException e) { Lantern.getLogger().warn("An error occurred while instantiating the profile cache file.", e); } this.cacheFile = cache; } private class ProfileCacheFile extends ConfigBase { @Setting(value = "entries") private List<ProfileCacheEntry> entries = new ArrayList<>(); ProfileCacheFile(Path path) throws IOException { super(path, false); } @Override public void save() throws IOException { synchronized (this) { this.entries.clear(); this.entries.addAll(byUUID.values().stream().collect(Collectors.toList())); this.entries.addAll(byName.values().stream().filter(e -> !this.entries.contains(e)).collect(Collectors.toList())); super.save(); } } @Override public void load() throws IOException { synchronized (this) { super.load(); byUUID.clear(); byName.clear(); this.entries.stream().filter(e -> !e.isExpired()).forEach(entry -> { byUUID.put(entry.gameProfile.getUniqueId(), entry); entry.gameProfile.getName().ifPresent(n -> byName.put(n, entry)); }); } } } @ConfigSerializable private static class ProfileCacheEntry { @Setting(value = "profile") private LanternGameProfile gameProfile; @Setting(value = "expiration-date") private Instant expirationDate; @Setting(value = "signed") private boolean signed; private ProfileCacheEntry() { } public ProfileCacheEntry(GameProfile profile, Instant expirationDate) { this.gameProfile = (LanternGameProfile) profile; this.expirationDate = expirationDate; } public boolean isExpired() { return Instant.now().compareTo(this.expirationDate) > 0; } } private Instant calculateDefaultExpirationDate() { return Instant.now().plus(EXPIRATION_DURATION); } /** * Saves the game profile cache. */ public void save() { try { this.cacheFile.save(); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while saving the profile cache file.", e); } } /** * TODO: SpongeAPI should use Instant? */ @Override public boolean add(GameProfile profile, boolean overwrite, @Nullable Date expiry) { final UUID uuid = checkNotNull(profile, "profile").getUniqueId(); if (overwrite && this.byUUID.containsKey(uuid)) { return false; } final Instant expirationDate; if (expiry != null) { expirationDate = expiry.toInstant(); } else { expirationDate = calculateDefaultExpirationDate(); } final ProfileCacheEntry entry = new ProfileCacheEntry(profile, expirationDate); this.byUUID.put(uuid, entry); profile.getName().ifPresent(name -> this.byName.put(name, entry)); return true; } @Override public boolean remove(GameProfile profile) { boolean flag = this.byUUID.remove(profile.getUniqueId()) != null; if (profile.getName().isPresent()) { flag = this.byName.remove(profile.getName().get()) != null || flag; } return flag; } @Override public Collection<GameProfile> remove(Iterable<GameProfile> profiles) { final ImmutableList.Builder<GameProfile> removed = ImmutableList.builder(); for (GameProfile profile : profiles) { if (this.remove(profile)) { removed.add(profile); } } return removed.build(); } @Override public void clear() { this.byName.clear(); this.byUUID.clear(); } @Override public Optional<GameProfile> getById(UUID uniqueId) { final ProfileCacheEntry entry = this.byUUID.get(checkNotNull(uniqueId, "uniqueId")); if (entry != null) { if (entry.isExpired()) { this.byUUID.remove(uniqueId, entry); entry.gameProfile.getName().ifPresent(name -> { ProfileCacheEntry entry1 = this.byName.get(name); if (entry == entry1) { this.byName.remove(name, entry); } }); } else { return Optional.of(entry.gameProfile); } } return Optional.empty(); } @Override public Map<UUID, Optional<GameProfile>> getByIds(Iterable<UUID> uniqueIds) { checkNotNull(uniqueIds, "uniqueIds"); final ImmutableMap.Builder<UUID, Optional<GameProfile>> builder = ImmutableMap.builder(); uniqueIds.forEach(uniqueId -> builder.put(uniqueId, getById(uniqueId))); return builder.build(); } @Override public Optional<GameProfile> lookupById(UUID uniqueId) { try { final GameProfile gameProfile = GameProfileQuery.queryProfileByUUID(uniqueId, true); add(gameProfile, true, null); this.byUUID.get(gameProfile.getUniqueId()).signed = true; return Optional.of(gameProfile); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while retrieving game profile data.", e); } catch (ProfileNotFoundException ignored) { } return Optional.empty(); } @Override public Map<UUID, Optional<GameProfile>> lookupByIds(Iterable<UUID> uniqueIds) { checkNotNull(uniqueIds, "uniqueIds"); final ImmutableMap.Builder<UUID, Optional<GameProfile>> builder = ImmutableMap.builder(); uniqueIds.forEach(uniqueId -> builder.put(uniqueId, this.lookupById(uniqueId))); return builder.build(); } @Override public Optional<GameProfile> getOrLookupById(UUID uniqueId) { final Optional<GameProfile> gameProfile = this.getById(checkNotNull(uniqueId, "uniqueId")); if (!gameProfile.isPresent()) { return this.lookupById(uniqueId); } return gameProfile; } @Override public Map<UUID, Optional<GameProfile>> getOrLookupByIds(Iterable<UUID> uniqueIds) { checkNotNull(uniqueIds, "uniqueIds"); final ImmutableMap.Builder<UUID, Optional<GameProfile>> builder = ImmutableMap.builder(); uniqueIds.forEach(uniqueId -> builder.put(uniqueId, getOrLookupById(uniqueId))); return builder.build(); } @Override public Optional<GameProfile> getByName(String name) { final ProfileCacheEntry entry = this.byName.get(checkNotNull(name, "name")); if (entry != null) { if (entry.isExpired()) { final UUID uniqueId = entry.gameProfile.getUniqueId(); final ProfileCacheEntry entry1 = this.byUUID.get(uniqueId); if (entry == entry1 || entry1.isExpired()) { this.byUUID.remove(uniqueId, entry); } this.byName.remove(name, entry); } else { return Optional.of(entry.gameProfile); } } return Optional.empty(); } @Override public Map<String, Optional<GameProfile>> getByNames(Iterable<String> names) { checkNotNull(names, "names"); final ImmutableMap.Builder<String, Optional<GameProfile>> builder = ImmutableMap.builder(); names.forEach(name -> builder.put(name, getByName(name))); return builder.build(); } @Override public Map<String, Optional<GameProfile>> lookupByNames(Iterable<String> names) { checkNotNull(names, "names"); final ImmutableMap.Builder<String, Optional<GameProfile>> result = ImmutableMap.builder(); lookupByNamesInto(result, Lists.newArrayList(names)); return result.build(); } @Override public Optional<GameProfile> getOrLookupByName(String name) { final Optional<GameProfile> gameProfile = getByName(checkNotNull(name, "name")); if (!gameProfile.isPresent()) { return lookupByName(name); } return gameProfile; } @Override public Map<String, Optional<GameProfile>> getOrLookupByNames(Iterable<String> names) { checkNotNull(names, "names"); final ImmutableMap.Builder<String, Optional<GameProfile>> result = ImmutableMap.builder(); final List<String> names0 = Lists.newArrayList(names); final Iterator<String> it = names0.iterator(); while (it.hasNext()) { final String name = it.next(); final Optional<GameProfile> gameProfile = getByName(name); if (gameProfile.isPresent()) { result.put(name, gameProfile); it.remove(); } } this.lookupByNamesInto(result, names0); return result.build(); } private void lookupByNamesInto(ImmutableMap.Builder<String, Optional<GameProfile>> builder, List<String> names) { try { final Map<String, UUID> namesResult = GameProfileQuery.queryUUIDByName(names); names.forEach(name -> { if (namesResult.containsKey(name)) { builder.put(name, lookupById(namesResult.get(name))); } else { builder.put(name, Optional.empty()); } }); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while retrieving game profile data.", e); } } @Override public Optional<GameProfile> lookupByName(String name) { try { final Map<String, UUID> result = GameProfileQuery.queryUUIDByName( Collections.singletonList(checkNotNull(name, "name"))); if (result.isEmpty()) { return Optional.empty(); } return lookupById(result.get(name)); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while retrieving game profile data.", e); } return Optional.empty(); } @Override public Optional<GameProfile> fillProfile(GameProfile profile, boolean signed) { try { final GameProfile gameProfile = GameProfileQuery.queryProfileByUUID( checkNotNull(profile, "profile").getUniqueId(), signed); profile.getPropertyMap().putAll(gameProfile.getPropertyMap()); final ProfileCacheEntry entry = this.byUUID.get(profile.getUniqueId()); if (entry == null || entry.isExpired() || (!entry.signed && signed)) { add(gameProfile, true, null); this.byUUID.get(gameProfile.getUniqueId()).signed = true; } return Optional.of(gameProfile); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while retrieving game profile data.", e); } catch (ProfileNotFoundException ignored) { } return Optional.empty(); } @Override public Collection<GameProfile> getProfiles() { final ImmutableList.Builder<GameProfile> builder = ImmutableList.builder(); final Iterator<Map.Entry<UUID, ProfileCacheEntry>> it = this.byUUID.entrySet().iterator(); while (it.hasNext()) { final ProfileCacheEntry entry = it.next().getValue(); if (entry.isExpired()) { entry.gameProfile.getName().ifPresent(this.byName::remove); it.remove(); } else { builder.add(entry.gameProfile); } } return builder.build(); } @Override public Collection<GameProfile> match(String name) { final String search = checkNotNull(name, "name").toLowerCase(Locale.ROOT); return getProfiles().stream() .filter(profile -> profile.getName().isPresent() && profile.getName().get().toLowerCase(Locale.ROOT).startsWith(search)) .collect(GuavaCollectors.toImmutableSet()); } }