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