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.backend;
018
019import ca.stellardrift.permissionsex.PermissionsEngine;
020import ca.stellardrift.permissionsex.datastore.DataStoreContext;
021import ca.stellardrift.permissionsex.context.ContextValue;
022import ca.stellardrift.permissionsex.impl.util.CacheListenerHolder;
023import ca.stellardrift.permissionsex.exception.PermissionsException;
024import ca.stellardrift.permissionsex.context.ContextInheritance;
025import ca.stellardrift.permissionsex.subject.ImmutableSubjectData;
026import ca.stellardrift.permissionsex.datastore.DataStore;
027import ca.stellardrift.permissionsex.datastore.DataStoreFactory;
028import ca.stellardrift.permissionsex.datastore.ProtoDataStore;
029import ca.stellardrift.permissionsex.exception.PermissionsLoadingException;
030import ca.stellardrift.permissionsex.rank.RankLadder;
031import ca.stellardrift.permissionsex.impl.util.Util;
032import ca.stellardrift.permissionsex.subject.SubjectRef;
033import ca.stellardrift.permissionsex.subject.SubjectType;
034import net.kyori.adventure.text.Component;
035import org.checkerframework.checker.nullness.qual.Nullable;
036import org.spongepowered.configurate.ConfigurationNode;
037import org.spongepowered.configurate.serialize.SerializationException;
038import org.spongepowered.configurate.util.CheckedSupplier;
039import org.spongepowered.configurate.util.UnmodifiableCollections;
040
041import java.util.Collections;
042import java.util.Map;
043import java.util.concurrent.CompletableFuture;
044import java.util.function.BiFunction;
045import java.util.function.Consumer;
046import java.util.function.Function;
047import java.util.stream.Stream;
048
049import static java.util.Objects.requireNonNull;
050
051/**
052 * Base implementation of a data store that provides common points for other data stores to hook into.
053 *
054 * @param <T> self type
055 * @param <C> config type
056 */
057public abstract class AbstractDataStore<T extends AbstractDataStore<T, C>, C> implements DataStore {
058    private final DataStoreContext context;
059    private final ProtoDataStore<C> properties;
060    private boolean firstRun;
061    protected final CacheListenerHolder<Map.Entry<String, String>, ImmutableSubjectData> listeners = new CacheListenerHolder<>();
062    protected final CacheListenerHolder<String, RankLadder> rankLadderListeners = new CacheListenerHolder<>();
063    protected final CacheListenerHolder<Boolean, ContextInheritance> contextInheritanceListeners = new CacheListenerHolder<>();
064
065    /**
066     * Create the data store.
067     *
068     * <p>No actual loading or creation of threads should be performed here.</p>
069     *
070     * @param context the data store context
071     * @param props properties defining this data store
072     */
073    protected AbstractDataStore(final DataStoreContext context, final ProtoDataStore<C> props) {
074        this.context = context;
075        this.properties = props;
076    }
077
078    @Override
079    public String name() {
080        return this.properties.identifier();
081    }
082
083    @Override
084    public boolean firstRun() {
085        return this.firstRun;
086    }
087
088    /**
089     * Mark that this is the data store's first run.
090     */
091    protected void markFirstRun() {
092        this.firstRun = true;
093    }
094
095    protected final PermissionsEngine engine() {
096        return this.context.engine();
097    }
098
099    protected final DataStoreContext context() {
100        return this.context;
101    }
102
103    protected C config() {
104        return this.properties.config();
105    }
106
107    /**
108     * Load any data necessary to initialize this data store.
109     *
110     * @throws PermissionsLoadingException if unable to load
111     */
112    protected abstract void load() throws PermissionsLoadingException;
113
114    @Override
115    public final CompletableFuture<ImmutableSubjectData> getData(final String type, final String identifier, final @Nullable Consumer<ImmutableSubjectData> listener) {
116        requireNonNull(type, "type");
117        requireNonNull(identifier, "identifier");
118
119        final CompletableFuture<ImmutableSubjectData> ret = getDataInternal(type, identifier);
120        ret.thenRun(() -> {
121            if (listener != null) {
122                listeners.addListener(UnmodifiableCollections.immutableMapEntry(type, identifier), listener);
123            }
124        });
125        return ret;
126    }
127
128    @Override
129    public final CompletableFuture<ImmutableSubjectData> setData(final String type, final String identifier, final @Nullable ImmutableSubjectData data) {
130        requireNonNull(type, "type");
131        requireNonNull(identifier, "identifier");
132
133        final Map.Entry<String, String> lookupKey = UnmodifiableCollections.immutableMapEntry(type, identifier);
134        return setDataInternal(type, identifier, data)
135                .thenApply(newData -> {
136                    if (newData != null) {
137                        listeners.call(lookupKey, newData);
138                    }
139                    return newData;
140                });
141    }
142
143    protected <V> CompletableFuture<V> runAsync(CheckedSupplier<V, ?> supplier) {
144        return Util.asyncFailableFuture(supplier, this.context.asyncExecutor());
145    }
146
147    protected CompletableFuture<Void> runAsync(Runnable run) {
148        return CompletableFuture.runAsync(run, this.context.asyncExecutor());
149    }
150
151    /**
152     * Apply default data when creating a new file.
153     *
154     * This consists of
155     * <ul>
156     *     <li>Modifying default data to give all permissions to a user connecting locally</li>
157     * </ul>
158     */
159    protected final void applyDefaultData() {
160        final SubjectRef<SubjectType<?>> defaultSubject = SubjectRef.subject(this.context().defaultsType(), this.context().defaultsType());
161        getData(defaultSubject, null)
162                .thenApply(data -> data.withSegment(Collections.singleton(new ContextValue<>("localip", "127.0.0.1")), s -> s.withFallbackPermission(1)))
163                .thenCompose(data -> setData(defaultSubject, data));
164    }
165
166    protected abstract CompletableFuture<ImmutableSubjectData> getDataInternal(String type, String identifier);
167
168    protected abstract CompletableFuture<ImmutableSubjectData> setDataInternal(String type, String identifier, @Nullable ImmutableSubjectData data);
169
170    @Override
171    public final Stream<Map.Entry<String, ImmutableSubjectData>> getAll(final String type) {
172        requireNonNull(type, "type");
173        return getAllIdentifiers(type)
174                .map(id -> UnmodifiableCollections.immutableMapEntry(id, getData(type, id, null).join()));
175    }
176
177    @Override
178    public final <V> CompletableFuture<V> performBulkOperation(final Function<DataStore, V> function) {
179        return Util.asyncFailableFuture(() -> performBulkOperationSync(function), this.context.asyncExecutor());
180    }
181
182    @Override
183    public final CompletableFuture<RankLadder> getRankLadder(final String ladderName, final @Nullable Consumer<RankLadder> listener) {
184        requireNonNull(ladderName, "ladderName");
185        CompletableFuture<RankLadder> ladder = getRankLadderInternal(ladderName);
186        if (listener != null) {
187            rankLadderListeners.addListener(ladderName.toLowerCase(), listener);
188        }
189        return ladder;
190    }
191
192    @Override
193    public final CompletableFuture<RankLadder> setRankLadder(final String identifier, final @Nullable RankLadder ladder) {
194        return setRankLadderInternal(identifier, ladder)
195                .thenApply(newData -> {
196                    if (newData != null) {
197                        rankLadderListeners.call(identifier, newData);
198                    }
199                    return newData;
200                });
201    }
202
203
204    protected abstract CompletableFuture<RankLadder> getRankLadderInternal(String ladder);
205    protected abstract CompletableFuture<RankLadder> setRankLadderInternal(String ladder, final @Nullable RankLadder newLadder);
206
207    @Override
208    public final CompletableFuture<ContextInheritance> getContextInheritance(final @Nullable Consumer<ContextInheritance> listener) {
209        CompletableFuture<ContextInheritance> inheritance = getContextInheritanceInternal();
210        if (listener != null) {
211            contextInheritanceListeners.addListener(true, listener);
212        }
213        return inheritance;
214    }
215
216
217    @Override
218    public final CompletableFuture<ContextInheritance> setContextInheritance(ContextInheritance contextInheritance) {
219        return setContextInheritanceInternal(contextInheritance)
220                .thenApply(newData -> {
221                    if (newData != null) {
222                        contextInheritanceListeners.call(true, newData);
223                    }
224                    return newData;
225                });
226    }
227
228
229
230    protected abstract CompletableFuture<ContextInheritance> getContextInheritanceInternal();
231    protected abstract CompletableFuture<ContextInheritance> setContextInheritanceInternal(ContextInheritance contextInheritance);
232
233    /**
234     * Internally perform a bulk operation.
235     *
236     * <p>Safe to call blocking operations from this method -- we're running it asyncly.</p>
237     *
238     * @param function The function to run
239     * @param <V> The
240     * @return result of operation, after a save
241     * @throws Exception if thrown by operation
242     */
243    protected abstract <V> V performBulkOperationSync(Function<DataStore, V> function) throws Exception;
244
245    @Override
246    public CompletableFuture<Void> moveData(String oldType, String oldIdentifier, String newType, String newIdentifier) {
247        return isRegistered(oldType, oldIdentifier).thenCombine(isRegistered(newType, newIdentifier), (oldRegistered, newRegistered) -> {
248            if (oldRegistered && !newRegistered) {
249                return getData(oldType, oldIdentifier, null)
250                        .thenCompose(oldData -> setData(newType, newIdentifier, oldData))
251                        .thenCompose(newData -> setData(oldType, oldIdentifier, null))
252                        .thenApply(inp -> (Void) null);
253            } else {
254                return Util.<Void>failedFuture(new PermissionsException(Messages.DATASTORE_MOVE_ERROR.tr()));
255            }
256
257        }).thenCompose(future -> future);
258    }
259
260    public abstract static class Factory<T extends AbstractDataStore<T, C>, C> implements DataStoreFactory<C> {
261        private final String name;
262        private final Class<C> configType;
263        /**
264         * Function taking the name of a data store instance and its configuration, and creating the appropriate new object.
265         */
266        private final BiFunction<DataStoreContext, ProtoDataStore<C>, T> newInstanceSupplier;
267        private final Component friendlyName;
268
269        protected Factory(final String name, final Class<C> clazz, final BiFunction<DataStoreContext, ProtoDataStore<C>, T> newInstanceSupplier) {
270            requireNonNull(name, "name");
271            requireNonNull(clazz, "clazz");
272            this.name = name;
273            this.friendlyName = Component.text(name);
274            this.configType = clazz;
275            this.newInstanceSupplier = newInstanceSupplier;
276        }
277
278        @Override
279        public Component friendlyName() {
280            return this.friendlyName;
281        }
282
283        @Override
284        public final String name() {
285            return this.name;
286        }
287
288        @Override
289        public final ProtoDataStore<C> create(String identifier, ConfigurationNode config) throws PermissionsLoadingException {
290            try {
291                final C dataStoreConfig = config.get(this.configType);
292                return ProtoDataStore.of(identifier, dataStoreConfig, this);
293            } catch (SerializationException e) {
294                throw new PermissionsLoadingException(Messages.DATASTORE_ERROR_DESERIALIZE.tr(identifier), e);
295            }
296        }
297
298        @Override
299        public DataStore defrost(final DataStoreContext context, final ProtoDataStore<C> properties) throws PermissionsLoadingException {
300            requireNonNull(context, "context");
301            final T store = this.newInstanceSupplier.apply(context, properties);
302            store.load();
303            return store;
304        }
305
306        @Override
307        public void serialize(final ConfigurationNode node, final ProtoDataStore<C> protoStore) throws SerializationException {
308            node.set(protoStore.config());
309        }
310
311    }
312}