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.subject;
018
019import ca.stellardrift.permissionsex.datastore.DataStore;
020import ca.stellardrift.permissionsex.impl.util.CacheListenerHolder;
021import ca.stellardrift.permissionsex.subject.ImmutableSubjectData;
022import ca.stellardrift.permissionsex.subject.InvalidIdentifierException;
023import ca.stellardrift.permissionsex.subject.SubjectDataCache;
024import ca.stellardrift.permissionsex.subject.SubjectRef;
025import ca.stellardrift.permissionsex.subject.SubjectType;
026import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
027import com.github.benmanes.caffeine.cache.Caffeine;
028import com.google.errorprone.annotations.concurrent.LazyInit;
029import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
030import org.checkerframework.checker.nullness.qual.Nullable;
031
032import java.util.Map;
033import java.util.concurrent.CompletableFuture;
034import java.util.concurrent.ConcurrentHashMap;
035import java.util.concurrent.atomic.AtomicReference;
036import java.util.function.Consumer;
037import java.util.function.UnaryOperator;
038import java.util.stream.Stream;
039
040import static java.util.Objects.requireNonNull;
041
042/**
043 * Cache for subject data objects from a single data store.
044 *
045 */
046public final class SubjectDataCacheImpl<I> implements SubjectDataCache<I> {
047    private final SubjectType<I> type;
048
049    @LazyInit
050    private DataStore dataStore;
051    private final AtomicReference<AsyncLoadingCache<I, ImmutableSubjectData>> cache = new AtomicReference<>();
052    /**
053     * Holds cache listeners to prevent them from being garbage-collected.
054     */
055    private final Map<I, Consumer<ImmutableSubjectData>> cacheHolders = new ConcurrentHashMap<>();
056    private final CacheListenerHolder<I, ImmutableSubjectData> listeners;
057    private final SubjectRef<SubjectType<?>> defaultIdentifier;
058
059    public SubjectDataCacheImpl(final SubjectType<I> type, final SubjectRef<SubjectType<?>> defaultIdentifier, final DataStore dataStore) {
060        this.type = type;
061        update(dataStore);
062        this.defaultIdentifier = defaultIdentifier;
063        this.listeners = new CacheListenerHolder<>();
064    }
065
066    /**
067     * For internal use only. Replace the backing data store while maintaining cache entries, ex. when the engine is reloaded.
068     *
069     * @param newDataStore The new data store to use
070     */
071    @EnsuresNonNull("this.dataStore")
072    public void update(final DataStore newDataStore) {
073        this.dataStore = newDataStore;
074        AsyncLoadingCache<I, ImmutableSubjectData> oldCache = this.cache.getAndSet(Caffeine.newBuilder()
075                        .maximumSize(512)
076                        .buildAsync((key, executor) -> dataStore.getData(this.type.name(), this.type.serializeIdentifier(key), clearListener(key))));
077        if (oldCache != null) {
078            oldCache.synchronous().asMap().forEach((k, v) -> {
079                    data(k, null).thenAccept(data -> listeners.call(k, data));
080                    // TODO: Not ignore this somehow? Add a listener in to the backend?
081            });
082        }
083    }
084
085    @Override
086    public CompletableFuture<ImmutableSubjectData> data(final I identifier, final @Nullable Consumer<ImmutableSubjectData> listener) {
087        requireNonNull(identifier, "identifier");
088
089        CompletableFuture<ImmutableSubjectData> ret = cache.get().get(identifier);
090        ret.thenRun(() -> {
091            if (listener != null) {
092                listeners.addListener(identifier, listener);
093            }
094        });
095        return ret;
096    }
097
098    @Override
099    public CompletableFuture<ToDataSubjectRefImpl<I>> referenceTo(final I identifier) {
100        return referenceTo(identifier, true);
101    }
102
103    @Override
104    public CompletableFuture<ToDataSubjectRefImpl<I>> referenceTo(final I identifier, final boolean strongListeners) {
105        final ToDataSubjectRefImpl<I> ref = new ToDataSubjectRefImpl<>(identifier, this, strongListeners);
106        return data(identifier, ref).thenApply(data -> {
107            ref.data.set(data);
108            return ref;
109        });
110    }
111
112    @Override
113    public CompletableFuture<ImmutableSubjectData> update(final I identifier, final UnaryOperator<ImmutableSubjectData> action) {
114        return data(identifier, null)
115                .thenCompose(data -> {
116                    ImmutableSubjectData newData = action.apply(data);
117                    if (data != newData) {
118                        return set(identifier, newData);
119                    } else {
120                        return CompletableFuture.completedFuture(data);
121                    }
122                });
123    }
124
125    @Override
126    public void load(final I identifier) {
127        requireNonNull(identifier, "identifier");
128
129        cache.get().get(identifier);
130    }
131
132    @Override
133    public void invalidate(final I identifier) {
134        requireNonNull(identifier, "identifier");
135
136        cache.get().synchronous().invalidate(identifier);
137        cacheHolders.remove(identifier);
138        listeners.removeAll(identifier);
139    }
140
141    @Override
142    public void cacheAll() {
143        dataStore.getAllIdentifiers(this.type.name()).forEach(ident -> {
144            try {
145                cache.get().synchronous().refresh(this.type.parseIdentifier(ident));
146            } catch (final InvalidIdentifierException ex) {
147                // TODO: log this
148            }
149        });
150    }
151
152    @Override
153    public CompletableFuture<Boolean> isRegistered(final I identifier) {
154        requireNonNull(identifier, "identifier");
155
156        return dataStore.isRegistered(this.type.name(), this.type.serializeIdentifier(identifier));
157    }
158
159    @Override
160    public CompletableFuture<ImmutableSubjectData> remove(final I identifier) {
161        return set(identifier, null);
162    }
163
164    @Override
165    public CompletableFuture<ImmutableSubjectData> set(final I identifier, final @Nullable ImmutableSubjectData newData) {
166        requireNonNull(identifier, "identifier");
167
168        return dataStore.setData(this.type.name(), this.type.serializeIdentifier(identifier), newData);
169    }
170
171    /**
172     * Create a new listener to pass to the backing data store. This listener will update our cache and notify all
173     * listeners to the cache that new data is available.
174     *
175     * @param name The subject identifier
176     * @return A caching function
177     */
178    private Consumer<ImmutableSubjectData> clearListener(final I name) {
179        Consumer<ImmutableSubjectData> ret = newData -> {
180            cache.get().put(name, CompletableFuture.completedFuture(newData));
181            listeners.call(name, newData);
182        };
183        cacheHolders.put(name, ret);
184        return ret;
185    }
186
187    @Override
188    public void addListener(final I identifier, final Consumer<ImmutableSubjectData> listener) {
189        requireNonNull(identifier, "identifier");
190        requireNonNull(listener, "listener");
191
192        listeners.addListener(identifier, listener);
193    }
194
195    @Override
196    public SubjectType<I> type() {
197        return this.type;
198    }
199
200    @Override
201    public Stream<I> getAllIdentifiers() {
202        return this.dataStore.getAllIdentifiers(type.name())
203                .map(this.type::parseIdentifier);
204    }
205
206    @Override
207    public SubjectRef<SubjectType<?>> getDefaultIdentifier() {
208        return this.defaultIdentifier;
209    }
210}