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}