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.file;
018
019import ca.stellardrift.permissionsex.datastore.DataStoreContext;
020import ca.stellardrift.permissionsex.datastore.ProtoDataStore;
021import ca.stellardrift.permissionsex.impl.backend.AbstractDataStore;
022import ca.stellardrift.permissionsex.datastore.DataStore;
023import ca.stellardrift.permissionsex.datastore.DataStoreFactory;
024import ca.stellardrift.permissionsex.impl.backend.memory.MemoryContextInheritance;
025import ca.stellardrift.permissionsex.context.ContextInheritance;
026import ca.stellardrift.permissionsex.impl.config.FilePermissionsExConfiguration;
027import ca.stellardrift.permissionsex.impl.config.SubjectRefSerializer;
028import ca.stellardrift.permissionsex.impl.util.PCollections;
029import ca.stellardrift.permissionsex.subject.ImmutableSubjectData;
030import ca.stellardrift.permissionsex.exception.PermissionsLoadingException;
031import ca.stellardrift.permissionsex.impl.rank.FixedRankLadder;
032import ca.stellardrift.permissionsex.rank.RankLadder;
033import ca.stellardrift.permissionsex.impl.util.Util;
034import ca.stellardrift.permissionsex.subject.SubjectRef;
035import com.google.auto.service.AutoService;
036import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
037import org.checkerframework.checker.nullness.qual.Nullable;
038import org.spongepowered.configurate.BasicConfigurationNode;
039import org.spongepowered.configurate.ConfigurateException;
040import org.spongepowered.configurate.ConfigurationNode;
041import org.spongepowered.configurate.gson.GsonConfigurationLoader;
042import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
043import org.spongepowered.configurate.loader.ConfigurationLoader;
044import org.spongepowered.configurate.objectmapping.ConfigSerializable;
045import org.spongepowered.configurate.objectmapping.meta.Comment;
046import org.spongepowered.configurate.serialize.SerializationException;
047import org.spongepowered.configurate.objectmapping.meta.Setting;
048import org.spongepowered.configurate.reference.ConfigurationReference;
049import org.spongepowered.configurate.reference.WatchServiceListener;
050import org.spongepowered.configurate.transformation.ConfigurationTransformation;
051import org.spongepowered.configurate.util.MapFactories;
052import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
053
054import java.io.IOException;
055import java.nio.file.Files;
056import java.nio.file.Path;
057import java.util.Collections;
058import java.util.Map;
059import java.util.Objects;
060import java.util.Set;
061import java.util.concurrent.CompletableFuture;
062import java.util.concurrent.atomic.AtomicBoolean;
063import java.util.concurrent.atomic.AtomicInteger;
064import java.util.function.Function;
065import java.util.stream.Collectors;
066import java.util.stream.Stream;
067
068import static java.util.concurrent.CompletableFuture.completedFuture;
069import static org.spongepowered.configurate.util.UnmodifiableCollections.immutableMapEntry;
070
071public final class FileDataStore extends AbstractDataStore<FileDataStore, FileDataStore.Config> {
072    static final String KEY_RANK_LADDERS = "rank-ladders";
073
074    @ConfigSerializable
075    static class Config {
076        @Setting
077        String file;
078        @Setting
079        @Comment("Place file entries in alphabetical order")
080        boolean alphabetizeEntries = false;
081        @Setting
082        @Comment("Automatically reload the data file when changes have been made")
083        boolean autoReload = true;
084    }
085
086
087    private @Nullable WatchServiceListener reloadService;
088    private @MonotonicNonNull ConfigurationReference<BasicConfigurationNode> permissionsConfig;
089    private final AtomicInteger saveSuppressed = new AtomicInteger();
090    private final AtomicBoolean dirty = new AtomicBoolean();
091
092    public FileDataStore(final DataStoreContext context, final ProtoDataStore<Config> properties) {
093        super(context, properties);
094    }
095
096    private ConfigurationReference<BasicConfigurationNode> createLoader(Path file) throws ConfigurateException {
097        Function<Path, ConfigurationLoader<? extends BasicConfigurationNode>> loaderFunc = path -> GsonConfigurationLoader.builder()
098                .defaultOptions(o -> {
099                    o = o.serializers(s -> FilePermissionsExConfiguration.populateSerializers(s)
100                            .register(SubjectRefSerializer.TYPE, new SubjectRefSerializer(this.context(), null)));
101                    if (config().alphabetizeEntries) {
102                        return o.mapFactory(MapFactories.sortedNatural());
103                    } else {
104                        return o;
105                    }
106                })
107                .path(path)
108                .indent(4)
109                .lenient(true)
110                .build();
111
112        ConfigurationReference<BasicConfigurationNode> ret;
113        if (this.reloadService != null) {
114            ret = this.reloadService.listenToConfiguration(loaderFunc, file);
115        } else {
116            ret = ConfigurationReference.fixed(loaderFunc.apply(file));
117        }
118
119        ret.updates().subscribe(this::refresh);
120
121        ret.errors().subscribe(e ->
122                context().logger().error(Messages.FILE_ERROR_AUTORELOAD.tr(e.getKey(), e.getValue().getLocalizedMessage())));
123
124        return ret;
125    }
126
127    /**
128     * Handle automatic reloads of the permissions storage
129     *
130     * @param newNode The updated node
131     */
132    private void refresh(ConfigurationNode newNode) {
133        this.listeners.getAllKeys().forEach(key -> {
134            try {
135                this.listeners.call(key, getDataSync(key.getKey(), key.getValue()));
136            } catch (PermissionsLoadingException e) {
137                this.context().logger().error(Messages.FILE_ERROR_SUBJECT_AUTORELOAD.tr(key.getKey(), key.getValue()));
138            }
139        });
140
141        this.rankLadderListeners.getAllKeys().forEach(key ->
142                this.rankLadderListeners.call(key, getRankLadderInternal(key).join()));
143
144        this.contextInheritanceListeners.getAllKeys().forEach(key ->
145                this.contextInheritanceListeners.call(key, getContextInheritanceInternal().join()));
146
147        this.context().logger().info(Messages.FILE_RELOAD_AUTO.tr(config().file));
148    }
149
150    private Path migrateLegacy(Path permissionsFile, String extension, ConfigurationLoader<?> legacyLoader, String formatName) throws PermissionsLoadingException {
151        Path legacyPermissionsFile = permissionsFile;
152        config().file = config().file.replace(extension, ".json");
153        permissionsFile = this.context().baseDirectory().resolve(config().file);
154        try {
155            permissionsConfig = createLoader(permissionsFile);
156            permissionsConfig.save(legacyLoader.load());
157            Files.move(legacyPermissionsFile, legacyPermissionsFile.resolveSibling(legacyPermissionsFile.getFileName().toString() + ".legacy-backup"));
158        } catch (final IOException e) {
159            throw new PermissionsLoadingException(Messages.FILE_ERROR_LEGACY_MIGRATION.tr(formatName, legacyPermissionsFile), e);
160        }
161        return permissionsFile;
162    }
163
164    @Override
165    protected void load() throws PermissionsLoadingException {
166        if (config().autoReload) {
167            try {
168                reloadService = WatchServiceListener.builder()
169                        .taskExecutor(this.context().asyncExecutor())
170                        .build();
171            } catch (IOException e) {
172                throw new PermissionsLoadingException(e);
173            }
174        }
175
176        final String rawFile = config().file;
177        Path permissionsFile = this.context().baseDirectory().resolve(rawFile);
178        if (rawFile.endsWith(".yml")) {
179            permissionsFile = migrateLegacy(permissionsFile, ".yml", YamlConfigurationLoader.builder().path(permissionsFile).build(), "YML");
180        } else if (rawFile.endsWith(".conf")) {
181            permissionsFile = migrateLegacy(permissionsFile, ".conf", HoconConfigurationLoader.builder().path(permissionsFile).build(), "HOCON");
182        } else {
183            try {
184                permissionsConfig = createLoader(permissionsFile);
185            } catch (final ConfigurateException e) {
186                throw new PermissionsLoadingException(Messages.FILE_ERROR_LOAD.tr(permissionsFile), e);
187            }
188        }
189
190        if (permissionsConfig.node().childrenMap().isEmpty()) { // New configuration, populate with default data
191            try {
192                performBulkOperationSync(input -> {
193                    applyDefaultData();
194                    permissionsConfig.get("schema-version").raw(SchemaMigrations.LATEST_VERSION);
195                    return null;
196                });
197            } catch (PermissionsLoadingException e) {
198                throw e;
199            } catch (Exception e) {
200                throw new PermissionsLoadingException(Messages.FILE_ERROR_INITIAL_DATA.tr(), e);
201            }
202            this.markFirstRun();
203        } else {
204            try {
205                ConfigurationTransformation versionUpdater = SchemaMigrations.versionedMigration(this.context().logger());
206                int startVersion = permissionsConfig.get("schema-version").getInt(-1);
207                ConfigurationNode node = permissionsConfig.node();
208                versionUpdater.apply(node);
209                int endVersion = permissionsConfig.get("schema-version").getInt();
210                if (endVersion > startVersion) {
211                    this.context().logger().info(Messages.FILE_SCHEMA_MIGRATION_SUCCESS.tr(permissionsFile, startVersion, endVersion));
212                    permissionsConfig.save(node);
213                }
214            } catch (final ConfigurateException ex) {
215                throw new PermissionsLoadingException(Messages.FILE_ERROR_SCHEMA_MIGRATION_SAVE.tr(), ex);
216            }
217        }
218    }
219
220    @Override
221    public void close() {
222        if (this.reloadService != null) {
223            try {
224                this.reloadService.close();
225            } catch (IOException e) {
226                this.context().logger().error("Unable to shut down FileDataStore watch service", e);
227            }
228            this.reloadService = null;
229        }
230    }
231
232    private ConfigurationNode getSubjectsNode() {
233        return this.permissionsConfig.get("subjects");
234    }
235
236    private CompletableFuture<Void> save() {
237        if (this.saveSuppressed.get() <= 0) {
238            return Util.asyncFailableFuture(() -> {
239                saveSync();
240                return null;
241            }, this.context().asyncExecutor());
242        } else {
243            return completedFuture(null);
244        }
245    }
246
247    private void saveSync() throws ConfigurateException {
248        if (this.saveSuppressed.get() <= 0) {
249            if (this.dirty.compareAndSet(true, false)) {
250                this.permissionsConfig.save();
251            }
252        }
253    }
254
255    @Override
256    public CompletableFuture<ImmutableSubjectData> getDataInternal(String type, String identifier) {
257        try {
258            return completedFuture(getDataSync(type, identifier));
259        } catch (PermissionsLoadingException e) {
260            return Util.failedFuture(e);
261        }
262    }
263
264    private ImmutableSubjectData getDataSync(String type, String identifier) throws PermissionsLoadingException {
265        try {
266            return FileSubjectData.fromNode(getSubjectsNode().node(type, identifier));
267        } catch (SerializationException e) {
268            throw new PermissionsLoadingException(Messages.FILE_ERROR_DESERIALIZE_SUBJECT.tr(), e);
269        }
270    }
271
272    @Override
273    protected CompletableFuture<ImmutableSubjectData> setDataInternal(String type, String identifier, final @Nullable ImmutableSubjectData data) {
274        try {
275            if (data == null) {
276                getSubjectsNode().node(type, identifier).raw(null);
277                dirty.set(true);
278                return save().thenApply(input -> null);
279            }
280
281            final FileSubjectData fileData;
282
283            if (data instanceof FileSubjectData) {
284                fileData = (FileSubjectData) data;
285            } else {
286                fileData = (FileSubjectData) new FileSubjectData().mergeFrom(data);
287            }
288            fileData.serialize(getSubjectsNode().node(type, identifier));
289            dirty.set(true);
290            return save().thenApply(none -> fileData);
291        } catch (SerializationException e) {
292            return Util.failedFuture(e);
293        }
294    }
295
296    @Override
297    public CompletableFuture<Boolean> isRegistered(String type, String identifier) {
298        return completedFuture(!getSubjectsNode().node(type, identifier).virtual());
299    }
300
301    @Override
302    public Stream<String> getAllIdentifiers(String type) {
303        return getSubjectsNode().node(type)
304                .childrenMap().keySet().stream()
305                .map(Objects::toString);
306    }
307
308    @Override
309    public Set<String> getRegisteredTypes() {
310        return Collections.unmodifiableSet(getSubjectsNode().childrenMap().entrySet().stream()
311                .filter(ent -> ent.getValue().isMap())
312                .map(Map.Entry::getKey)
313                .map(Object::toString)
314                .collect(Collectors.toSet()));
315    }
316
317    @Override
318    public CompletableFuture<Set<String>> getDefinedContextKeys() {
319        return CompletableFuture.completedFuture(getSubjectsNode().childrenMap().values().stream() // list of types
320                .flatMap(typeNode -> typeNode.childrenMap().values().stream()) // list of subjects
321                .flatMap(subjectNode -> subjectNode.childrenList().stream()) // list of segments
322                .flatMap(segmentNode -> segmentNode.node(FileSubjectData.KEY_CONTEXTS).childrenMap().entrySet().stream()) // list of contexts
323                .map(ctx -> ctx.getKey().toString()) // of context objets
324                .collect(PCollections.toPSet())); // to a set
325    }
326
327    @Override
328    public Stream<Map.Entry<SubjectRef<?>, ImmutableSubjectData>> getAll() {
329        return getSubjectsNode().childrenMap().keySet().stream() // all subject types
330                .flatMap(type -> { // for each subject type
331                    final String typeStr = type.toString();
332                    return getAll(typeStr).map(pair -> immutableMapEntry(this.context().deserializeSubjectRef(typeStr, pair.getKey()), pair.getValue()));
333                });
334    }
335
336    private ConfigurationNode getRankLaddersNode() {
337        return this.permissionsConfig.get(KEY_RANK_LADDERS);
338    }
339
340    @Override
341    public Stream<String> getAllRankLadders() {
342        return getRankLaddersNode().childrenMap().keySet()
343                .stream()
344                .map(Object::toString);
345    }
346
347    @Override
348    public CompletableFuture<RankLadder> getRankLadderInternal(String ladder) {
349        return completedFuture(new FixedRankLadder(ladder, getRankLaddersNode().node(ladder.toLowerCase()).childrenList().stream()
350                .map(node -> {
351                    try {
352                        return node.get(SubjectRef.TYPE);
353                    } catch (final SerializationException ex) {
354                        throw new RuntimeException(ex);
355                    }
356                })
357                .collect(PCollections.toPVector())));
358    }
359
360    @Override
361    public CompletableFuture<Boolean> hasRankLadder(String ladder) {
362        return completedFuture(!getRankLaddersNode().node(ladder.toLowerCase()).virtual());
363    }
364
365    @Override
366    public CompletableFuture<ContextInheritance> getContextInheritanceInternal() {
367        try {
368            return completedFuture(this.permissionsConfig.node().get(MemoryContextInheritance.class));
369        } catch (SerializationException e) {
370            return Util.failedFuture(e);
371        }
372    }
373
374    @Override
375    public CompletableFuture<ContextInheritance> setContextInheritanceInternal(final ContextInheritance inheritance) {
376        final MemoryContextInheritance realInheritance = MemoryContextInheritance.fromExistingContextInheritance(inheritance);
377        try {
378            this.permissionsConfig.node().set(MemoryContextInheritance.class, realInheritance);
379        } catch (SerializationException e) {
380            throw new RuntimeException(e);
381        }
382        dirty.set(true);
383        return save().thenApply(none -> realInheritance);
384    }
385
386    @Override
387    public CompletableFuture<RankLadder> setRankLadderInternal(final String identifier, final @Nullable RankLadder ladder) {
388        ConfigurationNode childNode = getRankLaddersNode().node(identifier.toLowerCase());
389        try {
390            childNode.raw(null);
391            if (ladder != null) {
392                for (final SubjectRef<?> rank : ladder.ranks()) {
393                    childNode.appendListNode().set(SubjectRef.TYPE, rank);
394                }
395            }
396        } catch (final SerializationException ex) {
397            return Util.failedFuture(ex);
398        }
399        this.dirty.set(true);
400        return save().thenApply(none -> ladder);
401    }
402
403    @Override
404    protected <T> T performBulkOperationSync(final Function<DataStore, T> function) throws Exception {
405        this.saveSuppressed.getAndIncrement();
406        T ret;
407        try {
408            ret = function.apply(this);
409        } finally {
410            this.saveSuppressed.getAndDecrement();
411        }
412        saveSync();
413        return ret;
414    }
415
416    @AutoService(DataStoreFactory.class)
417    public static class Factory extends AbstractDataStore.Factory<FileDataStore, Config> {
418        public Factory() {
419            super("file", Config.class, FileDataStore::new);
420        }
421    }
422}