/** * 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; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.sk89q.squirrelid.resolver.HttpRepositoryService; import com.sk89q.squirrelid.resolver.ProfileService; import ninja.leaping.permissionsex.backend.DataStore; import ninja.leaping.permissionsex.backend.memory.MemoryDataStore; import ninja.leaping.permissionsex.command.PermissionsExCommands; import ninja.leaping.permissionsex.command.RankingCommands; import ninja.leaping.permissionsex.config.PermissionsExConfiguration; import ninja.leaping.permissionsex.data.CacheListenerHolder; import ninja.leaping.permissionsex.data.Caching; import ninja.leaping.permissionsex.exception.PEBKACException; import ninja.leaping.permissionsex.logging.DebugPermissionCheckNotifier; import ninja.leaping.permissionsex.logging.PermissionCheckNotifier; import ninja.leaping.permissionsex.logging.RecordingPermissionCheckNotifier; import ninja.leaping.permissionsex.logging.TranslatableLogger; import ninja.leaping.permissionsex.data.ContextInheritance; import ninja.leaping.permissionsex.data.RankLadderCache; import ninja.leaping.permissionsex.data.SubjectCache; import ninja.leaping.permissionsex.exception.PermissionsLoadingException; import ninja.leaping.permissionsex.subject.SubjectType; import ninja.leaping.permissionsex.subject.SubjectTypeDefinition; import ninja.leaping.permissionsex.util.Util; import ninja.leaping.permissionsex.util.command.CommandSpec; import javax.sql.DataSource; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.file.Path; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import static ninja.leaping.permissionsex.util.Translations.*; public class PermissionsEx implements ImplementationInterface, Caching<ContextInheritance> { public static final String SUBJECTS_USER = "user"; public static final String SUBJECTS_GROUP = "group"; public static final String SUBJECTS_DEFAULTS = "default"; public static final ImmutableSet<Map.Entry<String, String>> GLOBAL_CONTEXT = ImmutableSet.of(); private final TranslatableLogger logger; private final ImplementationInterface impl; private final MemoryDataStore transientData; private final RecordingPermissionCheckNotifier baseNotifier = new RecordingPermissionCheckNotifier(); private volatile PermissionCheckNotifier notifier = baseNotifier; private final AtomicReference<State> state = new AtomicReference<>(); private final ConcurrentMap<String, SubjectType> subjectTypeCache = new ConcurrentHashMap<>(); private RankLadderCache rankLadderCache; private volatile CompletableFuture<ContextInheritance> cachedInheritance; private final CacheListenerHolder<Boolean, ContextInheritance> cachedInheritanceListeners = new CacheListenerHolder<>(); private static class State { private final PermissionsExConfiguration config; private final DataStore activeDataStore; private State(PermissionsExConfiguration config, DataStore activeDataStore) { this.config = config; this.activeDataStore = activeDataStore; } } public PermissionsEx(final PermissionsExConfiguration config, ImplementationInterface impl) throws PermissionsLoadingException { this.impl = impl; this.logger = TranslatableLogger.forLogger(impl.getLogger()); this.transientData = new MemoryDataStore(); this.transientData.initialize(this); setDebugMode(config.isDebugEnabled()); initialize(config); getSubjects(SUBJECTS_DEFAULTS).setTypeInfo(SubjectTypeDefinition.defaultFor(SUBJECTS_DEFAULTS, false)); convertUuids(); registerCommand(PermissionsExCommands.createRootCommand(this)); registerCommand(RankingCommands.getPromoteCommand(this)); registerCommand(RankingCommands.getDemoteCommand(this)); } private State getState() throws IllegalStateException { State ret = this.state.get(); if (ret == null) { throw new IllegalStateException("Manager has already been closed!"); } return ret; } private void convertUuids() { try { InetAddress.getByName("api.mojang.com"); final List<CompletableFuture<?>> actions = new ArrayList<>(); getState().activeDataStore.performBulkOperation(dataStore -> { Iterable<String> toConvert = Iterables.filter(dataStore.getAllIdentifiers(SUBJECTS_USER), input1 -> { if (input1 == null || input1.length() != 36) { return true; } try { UUID.fromString(input1); return false; } catch (IllegalArgumentException e) { return true; } }); if (toConvert.iterator().hasNext()) { getLogger().info(t("Trying to convert users stored by name to UUID")); } else { return 0; } final ProfileService service = HttpRepositoryService.forMinecraft(); try { final int[] converted = {0}; service.findAllByName(toConvert, profile -> { final String newIdentifier = profile.getUniqueId().toString(); String lookupName = profile.getName(); actions.add(dataStore.isRegistered(SUBJECTS_USER, newIdentifier).thenCombine( dataStore.isRegistered(SUBJECTS_USER, lookupName) .thenCombine(dataStore.isRegistered(SUBJECTS_USER, lookupName.toLowerCase()), (a, b) -> (a || b)), (newRegistered, oldRegistered) -> { if (newRegistered) { getLogger().warn(t("Duplicate entry for %s found while converting to UUID", newIdentifier + "/" + profile.getName())); return false; } else if (!oldRegistered) { return false; } converted[0]++; return true; }).thenCompose(doConvert -> { if (!doConvert) { return Util.emptyFuture(); } return dataStore.getData(SUBJECTS_USER, profile.getName(), null) .thenCompose(oldData -> { return dataStore.setData(SUBJECTS_USER, newIdentifier, oldData.setOption(GLOBAL_CONTEXT, "name", profile.getName())) .thenAccept(result -> dataStore.setData(SUBJECTS_USER, profile.getName(), null) .exceptionally(t -> { t.printStackTrace(); return null; })); }); })); return true; }); return converted[0]; } catch (IOException | InterruptedException e) { getLogger().error(t("Error while fetching UUIDs for users"), e); return 0; } }).thenCombine(CompletableFuture.allOf(actions.toArray(new CompletableFuture[actions.size()])), (count, none) -> count).thenAccept(result -> { if (result != null && result > 0) { getLogger().info(tn("%s user successfully converted from name to UUID", "%s users successfully converted from name to UUID!", result, result)); } }).exceptionally(t -> { getLogger().error(t("Error converting users to UUID"), t); return null; }); } catch (UnknownHostException e) { getLogger().warn(t("Unable to resolve Mojang API for UUID conversion. Do you have an internet connection? UUID conversion will not proceed (but may not be necessary).")); } } public SubjectType getSubjects(String type) { return subjectTypeCache.computeIfAbsent(type, key -> new SubjectType(this, type, new SubjectCache(type, getState().activeDataStore), new SubjectCache(type, transientData))); } public Collection<SubjectType> getActiveSubjectTypes() { return Collections.unmodifiableCollection(subjectTypeCache.values()); } /** * Access rank ladders through a cached interface * * @return Access to rank ladders */ public RankLadderCache getLadders() { return this.rankLadderCache; } /** * Imports data into the currently active backend from the backend identified by the provided identifier * * @param dataStoreIdentifier The identifier of the backend to import from * @return A future that completes once the import operation is complete */ public CompletableFuture<Void> importDataFrom(String dataStoreIdentifier) { final State state = getState(); final DataStore expected = state.config.getDataStore(dataStoreIdentifier); if (expected == null) { return Util.failedFuture(new IllegalArgumentException("Data store " + dataStoreIdentifier + " is not present")); } try { expected.initialize(this); } catch (PermissionsLoadingException e) { return Util.failedFuture(e); } return state.activeDataStore.performBulkOperation(store -> { CompletableFuture<Void> ret = CompletableFuture.allOf(Iterables.toArray(Iterables.transform(expected.getAll(), input -> store.setData(input.getKey().getKey(), input.getKey().getValue(), input.getValue())), CompletableFuture.class)) .thenCombine(expected.getContextInheritance(null).thenCompose(store::setContextInheritance), (v, a) -> null); for (String ladder : store.getAllRankLadders()) { ret = ret.thenCombine(expected.getRankLadder(ladder, null).thenCompose(ladderObj -> store.setRankLadder(ladder, ladderObj)), (v, a) -> null); } return ret; }).thenCompose(val -> Util.failableFuture(val::get)); } public Set<String> getRegisteredSubjectTypes() { return getState().activeDataStore.getRegisteredTypes(); } public PermissionCheckNotifier getNotifier() { return this.notifier; } public RecordingPermissionCheckNotifier getRecordingNotifier() { return this.baseNotifier; } // TODO: Proper thread-safety public boolean hasDebugMode() { return this.getNotifier() instanceof DebugPermissionCheckNotifier; } public void setDebugMode(boolean debug) { setDebugMode(debug, null); } public void setDebugMode(boolean debug, Pattern filterPattern) { if (debug) { if (!(this.notifier instanceof DebugPermissionCheckNotifier)) { this.notifier = new DebugPermissionCheckNotifier(getLogger(), this.notifier, filterPattern == null ? null : perm -> filterPattern.matcher(perm).find()); } } else { if (this.notifier instanceof DebugPermissionCheckNotifier) { this.notifier = ((DebugPermissionCheckNotifier) this.notifier).getDelegate(); } } } private void reloadSync() throws PEBKACException, PermissionsLoadingException { try { PermissionsExConfiguration config = getState().config.reload(); config.validate(); initialize(config); getSubjects(SUBJECTS_GROUP).cacheAll(); } catch (IOException e) { throw new PEBKACException(t("Error while loading configuration: %s", e.getLocalizedMessage())); } } private void initialize(PermissionsExConfiguration config) throws PermissionsLoadingException { State newState = new State(config, config.getDefaultDataStore()); newState.activeDataStore.initialize(this); try { newState.config.save(); } catch (IOException e) { throw new PermissionsLoadingException(t("Unable to write permissions configuration"), e); } State oldState = this.state.getAndSet(newState); if (oldState != null) { try { oldState.activeDataStore.close(); } catch (Exception e) {} // TODO maybe warn? } this.rankLadderCache = new RankLadderCache(this.rankLadderCache, newState.activeDataStore); this.subjectTypeCache.forEach((key, val) -> val.update(newState.activeDataStore)); getSubjects(SUBJECTS_GROUP).cacheAll(); if (this.cachedInheritance != null) { this.cachedInheritance = null; getContextInheritance(null).thenAccept(inheritance -> this.cachedInheritanceListeners.call(true, inheritance)); } // Migrate over legacy subject data newState.activeDataStore.moveData("system", SUBJECTS_DEFAULTS, SUBJECTS_DEFAULTS, SUBJECTS_DEFAULTS).thenRun(() -> { getLogger().info(t("Successfully migrated old-style default data to new location")); }); } public CompletableFuture<Void> reload() { return Util.asyncFailableFuture(() -> { reloadSync(); return null; }, getAsyncExecutor()); } public void close() { State state = this.state.getAndSet(null); state.activeDataStore.close(); } @Override public Path getBaseDirectory() { return impl.getBaseDirectory(); } @Override public TranslatableLogger getLogger() { return this.logger; } @Override public DataSource getDataSourceForURL(String url) throws SQLException { return impl.getDataSourceForURL(url); } /** * Get an executor to run tasks asynchronously on. * * @return The async executor */ @Override public Executor getAsyncExecutor() { return impl.getAsyncExecutor(); } @Override public void registerCommand(CommandSpec command) { impl.registerCommand(command); } @Override public Set<CommandSpec> getImplementationCommands() { return impl.getImplementationCommands(); } @Override public String getVersion() { return impl.getVersion(); } public PermissionsExConfiguration getConfig() { return getState().config; } public CompletableFuture<ContextInheritance> getContextInheritance(Caching<ContextInheritance> listener) { if (this.cachedInheritance == null) { this.cachedInheritance = getState().activeDataStore.getContextInheritance(this); } if (listener != null) { this.cachedInheritanceListeners.addListener(true, listener); } return this.cachedInheritance; } public CompletableFuture<ContextInheritance> setContextInheritance(ContextInheritance newInheritance) { return getState().activeDataStore.setContextInheritance(newInheritance); } @Override public void clearCache(ContextInheritance newData) { this.cachedInheritance = CompletableFuture.completedFuture(newData); this.cachedInheritanceListeners.call(true, newData); } }