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