001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.impl.rank;
018
019import ca.stellardrift.permissionsex.datastore.DataStore;
020import ca.stellardrift.permissionsex.impl.util.CacheListenerHolder;
021import ca.stellardrift.permissionsex.rank.RankLadder;
022import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
023import com.github.benmanes.caffeine.cache.Caffeine;
024import org.checkerframework.checker.nullness.qual.Nullable;
025
026import java.util.Map;
027import java.util.Objects;
028import java.util.concurrent.CompletableFuture;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.function.Consumer;
031import java.util.function.UnaryOperator;
032import java.util.stream.Stream;
033
034/**
035 * Access information about rank ladders.
036 */
037public class RankLadderCache implements ca.stellardrift.permissionsex.rank.RankLadderCollection {
038    private final DataStore dataStore;
039    private final AsyncLoadingCache<String, RankLadder> cache;
040    private final Map<String, Consumer<RankLadder>> cacheHolders = new ConcurrentHashMap<>();
041    private final CacheListenerHolder<String, RankLadder> listeners;
042
043    public RankLadderCache(final DataStore dataStore) {
044        this(null, dataStore);
045    }
046
047    public RankLadderCache(final @Nullable RankLadderCache existing, final DataStore dataStore) {
048        this.dataStore = dataStore;
049        cache = Caffeine.newBuilder()
050                .maximumSize(256)
051                .buildAsync((key, executor) -> dataStore.getRankLadder(key, clearListener(key)));
052        if (existing != null) {
053            listeners = existing.listeners;
054            existing.cache.synchronous().asMap().forEach((key, rankLadder) -> {
055                get(key, null).thenAccept(data -> listeners.call(key, data));
056            });
057        } else {
058            listeners = new CacheListenerHolder<>();
059        }
060    }
061
062    @Override
063    public CompletableFuture<RankLadder> get(final String identifier, final @Nullable Consumer<RankLadder> listener) {
064        Objects.requireNonNull(identifier, "identifier");
065
066        CompletableFuture<RankLadder> ret = cache.get(identifier);
067        ret.thenRun(() -> {
068            if (listener != null) {
069                listeners.addListener(identifier, listener);
070            }
071        });
072        return ret;
073    }
074
075    @Override
076    public CompletableFuture<RankLadder> update(final String identifier, final UnaryOperator<RankLadder> updateFunc) {
077        return cache.get(identifier)
078                .thenCompose(oldLadder -> {
079                    RankLadder newLadder = updateFunc.apply(oldLadder);
080                    if (oldLadder == newLadder) {
081                        return CompletableFuture.completedFuture(newLadder);
082                    }
083                    return set(identifier, newLadder);
084
085                });
086    }
087
088    public void load(final String identifier) {
089        Objects.requireNonNull(identifier, "identifier");
090        cache.synchronous().refresh(identifier);
091    }
092
093    public void invalidate(final String identifier) {
094        Objects.requireNonNull(identifier, "identifier");
095
096        cache.synchronous().invalidate(identifier);
097        cacheHolders.remove(identifier);
098        listeners.removeAll(identifier);
099    }
100
101    @Override
102    public CompletableFuture<Boolean> has(String identifier) {
103        Objects.requireNonNull(identifier, "identifier");
104
105        if (cache.synchronous().getIfPresent(identifier) != null) {
106            return CompletableFuture.completedFuture(true);
107        } else {
108            return dataStore.hasRankLadder(identifier);
109        }
110    }
111
112    @Override
113    public CompletableFuture<RankLadder> set(String identifier, RankLadder newData) {
114        Objects.requireNonNull(identifier, "identifier");
115
116        return dataStore.setRankLadder(identifier, newData);
117    }
118
119    private Consumer<RankLadder> clearListener(final String name) {
120        Consumer<RankLadder> ret = newData -> {
121            cache.synchronous().put(name, newData);
122            listeners.call(name, newData);
123        };
124        cacheHolders.put(name, ret);
125        return ret;
126    }
127
128    @Override
129    public void addListener(String identifier, Consumer<RankLadder> listener) {
130        Objects.requireNonNull(identifier, "identifier");
131        Objects.requireNonNull(listener, "listener");
132
133        listeners.addListener(identifier, listener);
134    }
135
136    @Override
137    public Stream<String> names() {
138        return dataStore.getAllRankLadders();
139    }
140}