Java tutorial
/* * This file is part of LuckPerms, licensed under the MIT License. * * Copyright (c) lucko (Luck) <luck@lucko.me> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package me.lucko.luckperms.common.storage.dao.file; import com.google.common.base.Throwables; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import me.lucko.luckperms.api.ChatMetaType; import me.lucko.luckperms.api.LogEntry; import me.lucko.luckperms.api.Node; import me.lucko.luckperms.api.PlayerSaveResult; import me.lucko.luckperms.api.context.ImmutableContextSet; import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.contexts.ContextSetConfigurateSerializer; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.NodeMapType; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.UserIdentifier; import me.lucko.luckperms.common.node.factory.NodeFactory; import me.lucko.luckperms.common.node.model.NodeDataContainer; import me.lucko.luckperms.common.node.utils.MetaType; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.dao.AbstractDao; import me.lucko.luckperms.common.storage.dao.file.loader.ConfigurateLoader; import me.lucko.luckperms.common.storage.dao.file.loader.JsonLoader; import me.lucko.luckperms.common.storage.dao.file.loader.YamlLoader; import me.lucko.luckperms.common.utils.ImmutableCollectors; import me.lucko.luckperms.common.utils.MoreFiles; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.SimpleConfigurationNode; import ninja.leaping.configurate.Types; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; /** * Abstract implementation using configurate {@link ConfigurationNode}s to serialize and deserialize * data. */ public abstract class AbstractConfigurateDao extends AbstractDao { // the loader responsible for i/o protected final ConfigurateLoader loader; // the name of the data directory private final String dataDirectoryName; // the data directory protected Path dataDirectory; // the uuid cache instance private final FileUuidCache uuidCache = new FileUuidCache(); // the action logger instance private final FileActionLogger actionLogger; // the file used to store uuid data private Path uuidDataFile; protected AbstractConfigurateDao(LuckPermsPlugin plugin, ConfigurateLoader loader, String name, String dataDirectoryName) { super(plugin, name); this.loader = loader; this.dataDirectoryName = dataDirectoryName; this.actionLogger = new FileActionLogger(plugin); } /** * Reads a configuration node from the given location * * @param location the location * @param name the name of the object * @return the node * @throws IOException if an io error occurs */ protected abstract ConfigurationNode readFile(StorageLocation location, String name) throws IOException; /** * Saves a configuration node to the given location * * @param location the location * @param name the name of the object * @param node the node * @throws IOException if an io error occurs */ protected abstract void saveFile(StorageLocation location, String name, ConfigurationNode node) throws IOException; // used to report i/o exceptions which took place in a specific file protected RuntimeException reportException(String file, Exception ex) throws RuntimeException { this.plugin.getLogger().warn("Exception thrown whilst performing i/o: " + file); ex.printStackTrace(); throw Throwables.propagate(ex); } @Override public void init() throws IOException { this.dataDirectory = this.plugin.getBootstrap().getDataDirectory().resolve(this.dataDirectoryName); MoreFiles.createDirectoriesIfNotExists(this.dataDirectory); this.uuidDataFile = MoreFiles.createFileIfNotExists(this.dataDirectory.resolve("uuidcache.txt")); this.uuidCache.load(this.uuidDataFile); this.actionLogger.init(this.dataDirectory.resolve("actions.json")); } @Override public void shutdown() { this.uuidCache.save(this.uuidDataFile); this.actionLogger.flush(); } @Override public void logAction(LogEntry entry) { this.actionLogger.logAction(entry); } @Override public Log getLog() throws IOException { return this.actionLogger.getLog(); } protected ConfigurationNode processBulkUpdate(BulkUpdate bulkUpdate, ConfigurationNode node) { Set<NodeDataContainer> nodes = readNodes(node); Set<NodeDataContainer> results = nodes.stream().map(bulkUpdate::apply).filter(Objects::nonNull) .collect(Collectors.toSet()); if (nodes.equals(results)) { return null; } writeNodes(node, results); return node; } @Override public User loadUser(UUID uuid, String username) { User user = this.plugin.getUserManager().getOrMake(UserIdentifier.of(uuid, username)); user.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.USER, uuid.toString()); if (object != null) { String name = object.getNode("name").getString(); user.getPrimaryGroup().setStoredValue(object .getNode(this.loader instanceof JsonLoader ? "primaryGroup" : "primary-group").getString()); Set<Node> nodes = readNodes(object).stream().map(NodeDataContainer::toNode) .collect(Collectors.toSet()); user.setNodes(NodeMapType.ENDURING, nodes); user.setName(name, true); boolean save = this.plugin.getUserManager().giveDefaultIfNeeded(user, false); if (user.getName().isPresent() && (name == null || !user.getName().get().equalsIgnoreCase(name))) { save = true; } if (save | user.auditTemporaryPermissions()) { saveUser(user); } } else { if (this.plugin.getUserManager().shouldSave(user)) { user.clearNodes(); user.getPrimaryGroup().setStoredValue(null); this.plugin.getUserManager().giveDefaultIfNeeded(user, false); } } } catch (Exception e) { throw reportException(uuid.toString(), e); } finally { user.invalidateCachedData(); user.getIoLock().unlock(); } return user; } @Override public void saveUser(User user) { user.getIoLock().lock(); try { if (!this.plugin.getUserManager().shouldSave(user)) { saveFile(StorageLocation.USER, user.getUuid().toString(), null); } else { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateDao) { data.getNode("uuid").setValue(user.getUuid().toString()); } data.getNode("name").setValue(user.getName().orElse("null")); data.getNode(this.loader instanceof JsonLoader ? "primaryGroup" : "primary-group") .setValue(user.getPrimaryGroup().getStoredValue().orElse(NodeFactory.DEFAULT_GROUP_NAME)); Set<NodeDataContainer> nodes = user.enduringData().immutable().values().stream() .map(NodeDataContainer::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); writeNodes(data, nodes); saveFile(StorageLocation.USER, user.getUuid().toString(), data); } } catch (Exception e) { throw reportException(user.getUuid().toString(), e); } finally { user.getIoLock().unlock(); } } @Override public Group createAndLoadGroup(String name) { Group group = this.plugin.getGroupManager().getOrMake(name); group.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.GROUP, name); if (object != null) { Set<Node> nodes = readNodes(object).stream().map(NodeDataContainer::toNode) .collect(Collectors.toSet()); group.setNodes(NodeMapType.ENDURING, nodes); } else { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateDao) { data.getNode("name").setValue(group.getName()); } Set<NodeDataContainer> nodes = group.enduringData().immutable().values().stream() .map(NodeDataContainer::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); writeNodes(data, nodes); saveFile(StorageLocation.GROUP, name, data); } } catch (Exception e) { throw reportException(name, e); } finally { group.invalidateCachedData(); group.getIoLock().unlock(); } return group; } @Override public Optional<Group> loadGroup(String name) { Group group = this.plugin.getGroupManager().getIfLoaded(name); if (group != null) { group.getIoLock().lock(); } try { ConfigurationNode object = readFile(StorageLocation.GROUP, name); if (object == null) { return Optional.empty(); } if (group == null) { group = this.plugin.getGroupManager().getOrMake(name); group.getIoLock().lock(); } Set<NodeDataContainer> data = readNodes(object); Set<Node> nodes = data.stream().map(NodeDataContainer::toNode).collect(Collectors.toSet()); group.setNodes(NodeMapType.ENDURING, nodes); } catch (Exception e) { throw reportException(name, e); } finally { if (group != null) { group.invalidateCachedData(); group.getIoLock().unlock(); } } return Optional.of(group); } @Override public void saveGroup(Group group) { group.getIoLock().lock(); try { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateDao) { data.getNode("name").setValue(group.getName()); } Set<NodeDataContainer> nodes = group.enduringData().immutable().values().stream() .map(NodeDataContainer::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); writeNodes(data, nodes); saveFile(StorageLocation.GROUP, group.getName(), data); } catch (Exception e) { throw reportException(group.getName(), e); } finally { group.getIoLock().unlock(); } } @Override public void deleteGroup(Group group) { group.getIoLock().lock(); try { saveFile(StorageLocation.GROUP, group.getName(), null); } catch (Exception e) { throw reportException(group.getName(), e); } finally { group.getIoLock().unlock(); } this.plugin.getGroupManager().unload(group); } @Override public Track createAndLoadTrack(String name) { Track track = this.plugin.getTrackManager().getOrMake(name); track.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.TRACK, name); if (object != null) { List<String> groups = object.getNode("groups").getChildrenList().stream() .map(ConfigurationNode::getString).collect(ImmutableCollectors.toList()); track.setGroups(groups); } else { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateDao) { data.getNode("name").setValue(name); } data.getNode("groups").setValue(track.getGroups()); saveFile(StorageLocation.TRACK, name, data); } } catch (Exception e) { throw reportException(name, e); } finally { track.getIoLock().unlock(); } return track; } @Override public Optional<Track> loadTrack(String name) { Track track = this.plugin.getTrackManager().getIfLoaded(name); if (track != null) { track.getIoLock().lock(); } try { ConfigurationNode object = readFile(StorageLocation.TRACK, name); if (object == null) { return Optional.empty(); } if (track == null) { track = this.plugin.getTrackManager().getOrMake(name); track.getIoLock().lock(); } List<String> groups = object.getNode("groups").getChildrenList().stream() .map(ConfigurationNode::getString).collect(ImmutableCollectors.toList()); track.setGroups(groups); } catch (Exception e) { throw reportException(name, e); } finally { if (track != null) { track.getIoLock().unlock(); } } return Optional.of(track); } @Override public void saveTrack(Track track) { track.getIoLock().lock(); try { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateDao) { data.getNode("name").setValue(track.getName()); } data.getNode("groups").setValue(track.getGroups()); saveFile(StorageLocation.TRACK, track.getName(), data); } catch (Exception e) { throw reportException(track.getName(), e); } finally { track.getIoLock().unlock(); } } @Override public void deleteTrack(Track track) { track.getIoLock().lock(); try { saveFile(StorageLocation.TRACK, track.getName(), null); } catch (Exception e) { throw reportException(track.getName(), e); } finally { track.getIoLock().unlock(); } this.plugin.getTrackManager().unload(track); } @Override public PlayerSaveResult savePlayerData(UUID uuid, String username) { return this.uuidCache.addMapping(uuid, username); } @Override public UUID getPlayerUuid(String username) { return this.uuidCache.lookupUuid(username); } @Override public String getPlayerName(UUID uuid) { return this.uuidCache.lookupUsername(uuid); } private static NodeDataContainer readAttributes(ConfigurationNode attributes, Function<ConfigurationNode, String> permissionFunction) { boolean value = attributes.getNode("value").getBoolean(true); String server = attributes.getNode("server").getString("global"); String world = attributes.getNode("world").getString("global"); long expiry = attributes.getNode("expiry").getLong(0L); ImmutableContextSet context = ImmutableContextSet.empty(); ConfigurationNode contextMap = attributes.getNode("context"); if (!contextMap.isVirtual() && contextMap.hasMapChildren()) { context = ContextSetConfigurateSerializer.deserializeContextSet(contextMap).makeImmutable(); } return NodeDataContainer.of(permissionFunction.apply(attributes), value, server, world, expiry, context); } private static Collection<NodeDataContainer> readAttributes(ConfigurationNode attributes, String permission) { boolean value = attributes.getNode("value").getBoolean(true); String server = attributes.getNode("server").getString("global"); String world = attributes.getNode("world").getString("global"); long expiry = attributes.getNode("expiry").getLong(0L); ImmutableContextSet context = ImmutableContextSet.empty(); ConfigurationNode contextMap = attributes.getNode("context"); if (!contextMap.isVirtual() && contextMap.hasMapChildren()) { context = ContextSetConfigurateSerializer.deserializeContextSet(contextMap).makeImmutable(); } ConfigurationNode batchAttribute = attributes.getNode("permissions"); if (permission.startsWith("luckperms.batch") && !batchAttribute.isVirtual() && batchAttribute.hasListChildren()) { List<NodeDataContainer> nodes = new ArrayList<>(); for (ConfigurationNode element : batchAttribute.getChildrenList()) { nodes.add(NodeDataContainer.of(element.getString(), value, server, world, expiry, context)); } return nodes; } else { return Collections.singleton(NodeDataContainer.of(permission, value, server, world, expiry, context)); } } private static Map.Entry<String, ConfigurationNode> parseEntry(ConfigurationNode appended, String keyFieldName) { if (!appended.hasMapChildren()) { return null; } Map<Object, ? extends ConfigurationNode> children = appended.getChildrenMap(); if (children.isEmpty()) { return null; } // if children.size == 1 and the only entry doesn't have a key called "permission" - assume // the key refers to the name of the permission if (children.size() == 1) { Map.Entry<Object, ? extends ConfigurationNode> entry = Iterables.getFirst(children.entrySet(), null); if (entry != null) { String permission = entry.getKey().toString(); ConfigurationNode attributes = entry.getValue(); if (!permission.equals(keyFieldName)) { return Maps.immutableEntry(permission, attributes); } } } // assume 'appended' is the actual entry. String permission = children.get(keyFieldName).getString(null); if (permission == null) { return null; } return Maps.immutableEntry(permission, appended); } protected static Set<NodeDataContainer> readNodes(ConfigurationNode data) { Set<NodeDataContainer> nodes = new HashSet<>(); if (data.getNode("permissions").hasListChildren()) { List<? extends ConfigurationNode> children = data.getNode("permissions").getChildrenList(); for (ConfigurationNode appended : children) { String plainValue = appended.getValue(Types::strictAsString); if (plainValue != null) { nodes.add(NodeDataContainer.of(plainValue)); continue; } Map.Entry<String, ConfigurationNode> entry = parseEntry(appended, "permission"); if (entry == null) { continue; } nodes.addAll(readAttributes(entry.getValue(), entry.getKey())); } } if (data.getNode("parents").hasListChildren()) { List<? extends ConfigurationNode> children = data.getNode("parents").getChildrenList(); for (ConfigurationNode appended : children) { String stringValue = appended.getValue(Types::strictAsString); if (stringValue != null) { nodes.add(NodeDataContainer.of(NodeFactory.groupNode(stringValue))); continue; } Map.Entry<String, ConfigurationNode> entry = parseEntry(appended, "group"); if (entry == null) { continue; } nodes.add(readAttributes(entry.getValue(), c -> NodeFactory.groupNode(entry.getKey()))); } } for (ChatMetaType chatMetaType : ChatMetaType.values()) { String keyName = chatMetaType.toString() + "es"; if (data.getNode(keyName).hasListChildren()) { List<? extends ConfigurationNode> children = data.getNode(keyName).getChildrenList(); for (ConfigurationNode appended : children) { Map.Entry<String, ConfigurationNode> entry = parseEntry(appended, chatMetaType.toString()); if (entry == null) { continue; } nodes.add(readAttributes(entry.getValue(), c -> NodeFactory.chatMetaNode(chatMetaType, c.getNode("priority").getInt(0), entry.getKey()))); } } } if (data.getNode("meta").hasListChildren()) { List<? extends ConfigurationNode> children = data.getNode("meta").getChildrenList(); for (ConfigurationNode appended : children) { Map.Entry<String, ConfigurationNode> entry = parseEntry(appended, "key"); if (entry == null) { continue; } nodes.add(readAttributes(entry.getValue(), c -> NodeFactory.metaNode(entry.getKey(), c.getNode("value").getString("null")))); } } return nodes; } private static void writeAttributesTo(ConfigurationNode attributes, NodeDataContainer node, boolean writeValue) { if (writeValue) { attributes.getNode("value").setValue(node.getValue()); } if (!node.getServer().equals("global")) { attributes.getNode("server").setValue(node.getServer()); } if (!node.getWorld().equals("global")) { attributes.getNode("world").setValue(node.getWorld()); } if (node.getExpiry() != 0L) { attributes.getNode("expiry").setValue(node.getExpiry()); } if (!node.getContexts().isEmpty()) { attributes.getNode("context") .setValue(ContextSetConfigurateSerializer.serializeContextSet(node.getContexts())); } } private static boolean isPlain(NodeDataContainer node) { return node.getValue() && node.getServer().equalsIgnoreCase("global") && node.getWorld().equalsIgnoreCase("global") && node.getExpiry() == 0L && node.getContexts().isEmpty(); } private void appendNode(ConfigurationNode base, String key, ConfigurationNode attributes, String keyFieldName) { if (this.loader instanceof YamlLoader) { // create a map node with a single entry of key --> attributes ConfigurationNode appended = base.getAppendedNode(); appended.getNode(key).setValue(attributes); } else { // include the attributes and key in the same map ConfigurationNode appended = base.getAppendedNode(); appended.getNode(keyFieldName).setValue(key); appended.mergeValuesFrom(attributes); } } private void writeNodes(ConfigurationNode to, Set<NodeDataContainer> nodes) { ConfigurationNode permissionsSection = SimpleConfigurationNode.root(); ConfigurationNode parentsSection = SimpleConfigurationNode.root(); ConfigurationNode prefixesSection = SimpleConfigurationNode.root(); ConfigurationNode suffixesSection = SimpleConfigurationNode.root(); ConfigurationNode metaSection = SimpleConfigurationNode.root(); for (NodeDataContainer node : nodes) { Node n = node.toNode(); // just add a string to the list. if (this.loader instanceof YamlLoader && isPlain(node)) { if (n.isGroupNode()) { parentsSection.getAppendedNode().setValue(n.getGroupName()); continue; } if (!MetaType.ANY.matches(n)) { permissionsSection.getAppendedNode().setValue(node.getPermission()); continue; } } ChatMetaType chatMetaType = ChatMetaType.ofNode(n).orElse(null); if (chatMetaType != null && n.getValue()) { // handle prefixes / suffixes Map.Entry<Integer, String> entry = chatMetaType.getEntry(n); ConfigurationNode attributes = SimpleConfigurationNode.root(); attributes.getNode("priority").setValue(entry.getKey()); writeAttributesTo(attributes, node, false); switch (chatMetaType) { case PREFIX: appendNode(prefixesSection, entry.getValue(), attributes, "prefix"); break; case SUFFIX: appendNode(suffixesSection, entry.getValue(), attributes, "suffix"); break; default: throw new AssertionError(); } } else if (n.isMeta() && n.getValue()) { // handle meta nodes Map.Entry<String, String> meta = n.getMeta(); ConfigurationNode attributes = SimpleConfigurationNode.root(); attributes.getNode("value").setValue(meta.getValue()); writeAttributesTo(attributes, node, false); appendNode(metaSection, meta.getKey(), attributes, "key"); } else if (n.isGroupNode() && n.getValue()) { // handle group nodes ConfigurationNode attributes = SimpleConfigurationNode.root(); writeAttributesTo(attributes, node, false); appendNode(parentsSection, n.getGroupName(), attributes, "group"); } else { // handle regular permissions and negated meta+prefixes+suffixes ConfigurationNode attributes = SimpleConfigurationNode.root(); writeAttributesTo(attributes, node, true); appendNode(permissionsSection, n.getPermission(), attributes, "permission"); } } if (permissionsSection.hasListChildren()) { to.getNode("permissions").setValue(permissionsSection); } if (parentsSection.hasListChildren()) { to.getNode("parents").setValue(parentsSection); } if (prefixesSection.hasListChildren()) { to.getNode("prefixes").setValue(prefixesSection); } if (suffixesSection.hasListChildren()) { to.getNode("suffixes").setValue(suffixesSection); } if (metaSection.hasListChildren()) { to.getNode("meta").setValue(metaSection); } } }