/** * PermissionsEx * Copyright (C) zml and PermissionsEx contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ninja.leaping.permissionsex.backend.file; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.reflect.TypeToken; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.ConfigurationOptions; import ninja.leaping.configurate.gson.GsonConfigurationLoader; import ninja.leaping.configurate.hocon.HoconConfigurationLoader; import ninja.leaping.configurate.loader.ConfigurationLoader; import ninja.leaping.configurate.objectmapping.ObjectMappingException; import ninja.leaping.configurate.objectmapping.Setting; import ninja.leaping.configurate.transformation.ConfigurationTransformation; import ninja.leaping.configurate.util.MapFactories; import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; import ninja.leaping.permissionsex.backend.AbstractDataStore; import ninja.leaping.permissionsex.backend.ConversionUtils; import ninja.leaping.permissionsex.backend.DataStore; import ninja.leaping.permissionsex.backend.memory.MemoryContextInheritance; import ninja.leaping.permissionsex.data.ContextInheritance; import ninja.leaping.permissionsex.data.ImmutableSubjectData; import ninja.leaping.permissionsex.exception.PermissionsLoadingException; import ninja.leaping.permissionsex.rank.FixedRankLadder; import ninja.leaping.permissionsex.rank.RankLadder; import ninja.leaping.permissionsex.util.GuavaCollectors; import ninja.leaping.permissionsex.util.Util; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import static java.util.concurrent.CompletableFuture.completedFuture; import static ninja.leaping.permissionsex.util.Translations.t; public final class FileDataStore extends AbstractDataStore { public static final String KEY_RANK_LADDERS = "rank-ladders"; public static final Factory FACTORY = new Factory("file", FileDataStore.class); @Setting private String file; @Setting(value = "alphabetize-entries", comment = "Place file entries in alphabetical order") private boolean alphabetizeEntries = false; private ConfigurationLoader permissionsFileLoader; private ConfigurationNode permissionsConfig; private final AtomicInteger saveSuppressed = new AtomicInteger(); private final AtomicBoolean dirty = new AtomicBoolean(); public FileDataStore() { super(FACTORY); } private ConfigurationLoader<? extends ConfigurationNode> createLoader(Path file) { return GsonConfigurationLoader.builder() .setPath(file) .setIndent(4) .setLenient(true) .build(); } private ConfigurationOptions getLoadOptions() { ConfigurationOptions ret = ConfigurationOptions.defaults(); if (alphabetizeEntries) { ret = ret.setMapFactory(MapFactories.sortedNatural()); } return ret; } private Path migrateLegacy(Path permissionsFile, String extension, ConfigurationLoader<?> loader, String formatName) throws PermissionsLoadingException { Path legacyPermissionsFile = permissionsFile; file = file.replace(extension, ".json"); permissionsFile = getManager().getBaseDirectory().resolve(file); permissionsFileLoader = createLoader(permissionsFile); try { permissionsConfig = loader.load(); permissionsFileLoader.save(permissionsConfig); Files.move(legacyPermissionsFile, legacyPermissionsFile.resolveSibling(legacyPermissionsFile.getFileName().toString() + ".legacy-backup")); } catch (IOException e) { throw new PermissionsLoadingException(t("While loading legacy %s permissions from %s", formatName, legacyPermissionsFile), e); } return permissionsFile; } @Override protected void initializeInternal() throws PermissionsLoadingException { Path permissionsFile = getManager().getBaseDirectory().resolve(file); if (file.endsWith(".yml")) { permissionsFile = migrateLegacy(permissionsFile, ".yml", YAMLConfigurationLoader.builder().setPath(permissionsFile).build(), "YML"); } else if (file.endsWith(".conf")) { permissionsFile = migrateLegacy(permissionsFile, ".conf", HoconConfigurationLoader.builder().setPath(permissionsFile).build(), "HOCON"); } else { permissionsFileLoader = createLoader(permissionsFile); } try { permissionsConfig = permissionsFileLoader.load(getLoadOptions()); } catch (IOException e) { throw new PermissionsLoadingException(t("While loading permissions file from %s", permissionsFile), e); } if (permissionsConfig.getChildrenMap().isEmpty()) { // New configuration, populate with default data try { performBulkOperationSync(input -> { applyDefaultData(); permissionsConfig.getNode("schema-version").setValue(SchemaMigrations.LATEST_VERSION); return null; }); } catch (PermissionsLoadingException e) { throw e; } catch (Exception e) { throw new PermissionsLoadingException(t("Error creating initial data for file backend"), e); } } else { ConfigurationTransformation versionUpdater = SchemaMigrations.versionedMigration(getManager().getLogger()); int startVersion = permissionsConfig.getNode("schema-version").getInt(-1); versionUpdater.apply(permissionsConfig); int endVersion = permissionsConfig.getNode("schema-version").getInt(); if (endVersion > startVersion) { getManager().getLogger().info(t("%s schema version updated from %s to %s", permissionsFile, startVersion, endVersion)); try { save().get(); } catch (InterruptedException | ExecutionException e) { throw new PermissionsLoadingException(t("While performing version upgrade"), e); } } } } @Override public void close() { } private ConfigurationNode getSubjectsNode() { return this.permissionsConfig.getNode("subjects"); } private CompletableFuture<Void> save() { if (saveSuppressed.get() <= 0) { return Util.asyncFailableFuture(() -> { saveSync(); return null; }, getManager().getAsyncExecutor()); } else { return completedFuture(null); } } private void saveSync() throws IOException { if (saveSuppressed.get() <= 0) { if (dirty.compareAndSet(true, false)) { permissionsFileLoader.save(permissionsConfig); } } } @Override public CompletableFuture<ImmutableSubjectData> getDataInternal(String type, String identifier) { try { return completedFuture(FileSubjectData.fromNode(getSubjectsNode().getNode(type, identifier))); } catch (PermissionsLoadingException e) { return Util.failedFuture(e); } catch (ObjectMappingException e) { return Util.failedFuture(new PermissionsLoadingException(t("While deserializing subject data for %s:", identifier), e)); } } @Override protected CompletableFuture<ImmutableSubjectData> setDataInternal(String type, String identifier, final ImmutableSubjectData data) { try { if (data == null) { getSubjectsNode().getNode(type, identifier).setValue(null); dirty.set(true); return save().thenApply(input -> null); } final FileSubjectData fileData; if (data instanceof FileSubjectData) { fileData = (FileSubjectData) data; } else { fileData = ConversionUtils.transfer(data, new FileSubjectData()); } fileData.serialize(getSubjectsNode().getNode(type, identifier)); dirty.set(true); return save().thenApply(none -> fileData); } catch (ObjectMappingException e) { return Util.failedFuture(e); } } @Override public CompletableFuture<Boolean> isRegistered(String type, String identifier) { return completedFuture(!getSubjectsNode().getNode(type, identifier).isVirtual()); } @Override @SuppressWarnings("unchecked") public Set<String> getAllIdentifiers(String type) { return (Set) getSubjectsNode().getNode(type).getChildrenMap().keySet(); } @Override public Set<String> getRegisteredTypes() { return getSubjectsNode().getChildrenMap().entrySet().stream() .filter(ent -> ent.getValue().hasMapChildren()) .map(Map.Entry::getKey) .map(Object::toString) .distinct() .collect(GuavaCollectors.toImmutableSet()); } @Override public Iterable<Map.Entry<Map.Entry<String, String>, ImmutableSubjectData>> getAll() { /*return getSubjectsNode().getChildrenMap().keySet().stream() .filter(i -> i != null) .flatMap(type -> { final String typeStr = type.toString(); return getAll(typeStr) .stream() .map(ent -> Maps.immutableEntry(Maps.immutableEntry(typeStr, ent.getKey()), ent.getValue())); }) .collect(GuavaCollectors.toImmutableSet());*/ return Iterables.concat(Iterables.transform(getSubjectsNode().getChildrenMap().keySet(), type -> { if (type == null) { return null; } final String typeStr = type.toString(); return Iterables.transform(getAll(typeStr), input2 -> Maps.immutableEntry(Maps.immutableEntry(type.toString(), input2.getKey()), input2.getValue())); })); } private ConfigurationNode getRankLaddersNode() { return this.permissionsConfig.getNode(KEY_RANK_LADDERS); } @Override public Iterable<String> getAllRankLadders() { return Iterables.unmodifiableIterable(Iterables.transform(getRankLaddersNode().getChildrenMap().keySet(), Object::toString)); } @Override public CompletableFuture<RankLadder> getRankLadderInternal(String ladder) { return completedFuture(new FixedRankLadder(ladder, ImmutableList.copyOf(Lists.transform(getRankLaddersNode().getNode(ladder.toLowerCase()).getChildrenList(), input -> Util.subjectFromString(input.getString()))))); } @Override public CompletableFuture<Boolean> hasRankLadder(String ladder) { return completedFuture(!getRankLaddersNode().getNode(ladder.toLowerCase()).isVirtual()); } @Override public CompletableFuture<ContextInheritance> getContextInheritanceInternal() { try { return completedFuture(this.permissionsConfig.getValue(TypeToken.of(MemoryContextInheritance.class))); } catch (ObjectMappingException e) { return Util.failedFuture(e); } } @Override public CompletableFuture<ContextInheritance> setContextInheritanceInternal(final ContextInheritance inheritance) { final MemoryContextInheritance realInheritance = MemoryContextInheritance.fromExistingContextInheritance(inheritance); try { this.permissionsConfig.setValue(TypeToken.of(MemoryContextInheritance.class), realInheritance); } catch (ObjectMappingException e) { throw new RuntimeException(e); } dirty.set(true); return save().thenApply(none -> realInheritance); } @Override public CompletableFuture<RankLadder> setRankLadderInternal(String identifier, RankLadder ladder) { ConfigurationNode childNode = getRankLaddersNode().getNode(identifier.toLowerCase()); childNode.setValue(null); if (ladder != null) { for (Map.Entry<String, String> rank : ladder.getRanks()) { childNode.getAppendedNode().setValue(Util.subjectToString(rank)); } } dirty.set(true); return save().thenApply(none -> ladder); } @Override protected <T> T performBulkOperationSync(Function<DataStore, T> function) throws Exception { saveSuppressed.getAndIncrement(); T ret; try { ret = function.apply(this); } finally { saveSuppressed.getAndDecrement(); } saveSync(); return ret; } }