All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cloud.commandframework.CommandTree Maven / Gradle / Ivy

There is a newer version: 1.8.4
Show newest version
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & 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 cloud.commandframework;

import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.StaticArgument;
import cloud.commandframework.arguments.compound.CompoundArgument;
import cloud.commandframework.arguments.compound.FlagArgument;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.exceptions.AmbiguousNodeException;
import cloud.commandframework.exceptions.ArgumentParseException;
import cloud.commandframework.exceptions.InvalidCommandSenderException;
import cloud.commandframework.exceptions.InvalidSyntaxException;
import cloud.commandframework.exceptions.NoCommandInLeafException;
import cloud.commandframework.exceptions.NoPermissionException;
import cloud.commandframework.exceptions.NoSuchCommandException;
import cloud.commandframework.permission.CommandPermission;
import cloud.commandframework.permission.OrPermission;
import cloud.commandframework.types.tuples.Pair;
import io.leangen.geantyref.GenericTypeReflector;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.stream.Collectors;

/**
 * Tree containing all commands and command paths.
 * 

* All {@link Command commands} consists of unique paths made out of {@link CommandArgument arguments}. * These arguments may be {@link StaticArgument literals} or variables. Command may either be required * or optional, with the requirement that no optional argument precedes a required argument. *

* The {@link Command commands} are stored in this tree and the nodes of tree consists of the command * {@link CommandArgument arguments}. Each leaf node of the tree should containing a fully parsed * {@link Command}. It is thus possible to walk the tree and determine whether or not the supplied * input from a command sender constitutes a proper command. *

* When parsing input, the tree will be walked until one of four scenarios occur: *

    *
  1. The input queue is empty at a non-leaf node
  2. *
  3. The input queue is not empty following a leaf node
  4. *
  5. No child node is able to accept the input
  6. *
  7. The input queue is empty following a leaf node
  8. *
*

* Scenarios one and two would result in a {@link InvalidSyntaxException} being thrown, whereas * scenario three would result in a {@link NoSuchCommandException} if occurring at the root node * or a {@link InvalidSyntaxException} otherwise. Only the fourth scenario would result in a complete * command being parsed. * * @param Command sender type */ public final class CommandTree { private final Object commandLock = new Object(); private final Node> internalTree = new Node<>(null); private final CommandManager commandManager; private CommandTree(final @NonNull CommandManager commandManager) { this.commandManager = commandManager; } /** * Create a new command tree instance * * @param commandManager Command manager * @param Command sender type * @return New command tree */ public static @NonNull CommandTree newTree(final @NonNull CommandManager commandManager) { return new CommandTree<>(commandManager); } /** * Attempt to parse string input into a command * * @param commandContext Command context instance * @param args Input * @return Parsed command, if one could be found */ public @NonNull Pair<@Nullable Command, @Nullable Exception> parse( final @NonNull CommandContext commandContext, final @NonNull Queue<@NonNull String> args ) { final Pair<@Nullable Command, @Nullable Exception> pair = this.parseCommand( new ArrayList<>(), commandContext, args, this.internalTree ); if (pair.getFirst() != null) { final Command command = pair.getFirst(); if (command.getSenderType().isPresent() && !command.getSenderType().get() .isAssignableFrom(commandContext .getSender() .getClass())) { return Pair.of(null, new InvalidCommandSenderException( commandContext.getSender(), command.getSenderType().get(), Collections.emptyList() )); } } return pair; } private @NonNull Pair<@Nullable Command, @Nullable Exception> parseCommand( final @NonNull List<@NonNull CommandArgument> parsedArguments, final @NonNull CommandContext commandContext, final @NonNull Queue<@NonNull String> commandQueue, final @NonNull Node<@Nullable CommandArgument> root ) { CommandPermission permission = this.isPermitted(commandContext.getSender(), root); if (permission != null) { return Pair.of(null, new NoPermissionException( permission, commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } final Pair<@Nullable Command, @Nullable Exception> parsedChild = this.attemptParseUnambiguousChild( parsedArguments, commandContext, root, commandQueue ); if (parsedChild.getFirst() != null || parsedChild.getSecond() != null) { return parsedChild; } /* There are 0 or more static arguments as children. No variable child arguments are present */ if (root.children.isEmpty()) { /* We are at the bottom. Check if there's a command attached, in which case we're done */ if (root.getValue() != null && root.getValue().getOwningCommand() != null) { if (commandQueue.isEmpty()) { return Pair.of(this.cast(root.getValue().getOwningCommand()), null); } else { /* Too many arguments. We have a unique path, so we can send the entire context */ return Pair.of(null, new InvalidSyntaxException( this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } } else { /* Too many arguments. We have a unique path, so we can send the entire context */ return Pair.of(null, new InvalidSyntaxException( this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } } else { final Iterator>> childIterator = root.getChildren().iterator(); if (childIterator.hasNext()) { while (childIterator.hasNext()) { final Node> child = childIterator.next(); if (child.getValue() != null) { final CommandArgument argument = child.getValue(); final CommandContext.ArgumentTiming argumentTiming = commandContext.createTiming(argument); argumentTiming.setStart(System.nanoTime()); final ArgumentParseResult result = argument.getParser().parse(commandContext, commandQueue); argumentTiming.setEnd(System.nanoTime(), result.getFailure().isPresent()); if (result.getParsedValue().isPresent()) { parsedArguments.add(child.getValue()); return this.parseCommand(parsedArguments, commandContext, commandQueue, child); } } } } /* We could not find a match */ if (root.equals(this.internalTree)) { return Pair.of(null, new NoSuchCommandException( commandContext.getSender(), getChain(root).stream().map(Node::getValue).collect(Collectors.toList()), stringOrEmpty(commandQueue.peek()) )); } /* If we couldn't match a child, check if there's a command attached and execute it */ if (root.getValue() != null && root.getValue().getOwningCommand() != null && commandQueue.isEmpty()) { final Command command = root.getValue().getOwningCommand(); if (!this.getCommandManager().hasPermission( commandContext.getSender(), command.getCommandPermission() )) { return Pair.of(null, new NoPermissionException( command.getCommandPermission(), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } return Pair.of(root.getValue().getOwningCommand(), null); } /* We know that there's no command and we also cannot match any of the children */ return Pair.of(null, new InvalidSyntaxException( this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } } private @NonNull Pair<@Nullable Command, @Nullable Exception> attemptParseUnambiguousChild( final @NonNull List<@NonNull CommandArgument> parsedArguments, final @NonNull CommandContext commandContext, final @NonNull Node<@Nullable CommandArgument> root, final @NonNull Queue commandQueue ) { CommandPermission permission; final List>> children = root.getChildren(); if (children.size() == 1 && !(children.get(0).getValue() instanceof StaticArgument)) { // The value has to be a variable final Node> child = children.get(0); permission = this.isPermitted(commandContext.getSender(), child); if (!commandQueue.isEmpty() && permission != null) { return Pair.of(null, new NoPermissionException( permission, commandContext.getSender(), this.getChain(child) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } if (child.getValue() != null) { if (commandQueue.isEmpty()) { if (child.getValue().hasDefaultValue()) { commandQueue.add(child.getValue().getDefaultValue()); } else if (!child.getValue().isRequired()) { return Pair.of(this.cast(child.getValue().getOwningCommand()), null); } else if (child.isLeaf()) { if (root.getValue() != null && root.getValue().getOwningCommand() != null) { final Command command = root.getValue().getOwningCommand(); if (!this.getCommandManager().hasPermission( commandContext.getSender(), command.getCommandPermission() )) { return Pair.of(null, new NoPermissionException( command.getCommandPermission(), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } return Pair.of(command, null); } /* Not enough arguments */ return Pair.of(null, new InvalidSyntaxException( this.commandManager.getCommandSyntaxFormatter() .apply(Objects.requireNonNull( child.getValue() .getOwningCommand()) .getArguments(), child), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } else { /* The child is not a leaf, but may have an intermediary executor, attempt to use it */ if (root.getValue() != null && root.getValue().getOwningCommand() != null) { final Command command = root.getValue().getOwningCommand(); if (!this.getCommandManager().hasPermission( commandContext.getSender(), command.getCommandPermission() )) { return Pair.of(null, new NoPermissionException( command.getCommandPermission(), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } return Pair.of(command, null); } /* Child does not have a command and so we cannot proceed */ return Pair.of(null, new InvalidSyntaxException( this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, root), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } } final CommandArgument argument = child.getValue(); final CommandContext.ArgumentTiming argumentTiming = commandContext.createTiming(argument); // START: Parsing argumentTiming.setStart(System.nanoTime()); final ArgumentParseResult result; final ArgumentParseResult preParseResult = child.getValue().preprocess( commandContext, commandQueue ); if (!preParseResult.getFailure().isPresent() && preParseResult.getParsedValue().orElse(false)) { result = argument.getParser().parse(commandContext, commandQueue); } else { result = preParseResult; } argumentTiming.setEnd(System.nanoTime(), result.getFailure().isPresent()); // END: Parsing if (result.getParsedValue().isPresent()) { commandContext.store(child.getValue().getName(), result.getParsedValue().get()); if (child.isLeaf()) { if (commandQueue.isEmpty()) { return Pair.of(this.cast(child.getValue().getOwningCommand()), null); } else { /* Too many arguments. We have a unique path, so we can send the entire context */ return Pair.of(null, new InvalidSyntaxException( this.commandManager.getCommandSyntaxFormatter() .apply(parsedArguments, child), commandContext.getSender(), this.getChain(root) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } } else { parsedArguments.add(child.getValue()); return this.parseCommand(parsedArguments, commandContext, commandQueue, child); } } else if (result.getFailure().isPresent()) { return Pair.of(null, new ArgumentParseException( result.getFailure().get(), commandContext.getSender(), this.getChain(child) .stream() .filter(node -> node.getValue() != null) .map(Node::getValue) .collect(Collectors.toList()) )); } } } return Pair.of(null, null); } /** * Get suggestions from the input queue * * @param context Context instance * @param commandQueue Input queue * @return String suggestions. These should be filtered based on {@link String#startsWith(String)} */ public @NonNull List<@NonNull String> getSuggestions( final @NonNull CommandContext context, final @NonNull Queue<@NonNull String> commandQueue ) { return getSuggestions(context, commandQueue, this.internalTree); } private @NonNull List<@NonNull String> getSuggestions( final @NonNull CommandContext commandContext, final @NonNull Queue<@NonNull String> commandQueue, final @NonNull Node<@Nullable CommandArgument> root ) { /* If the sender isn't allowed to access the root node, no suggestions are needed */ if (this.isPermitted(commandContext.getSender(), root) != null) { return Collections.emptyList(); } final List>> children = root.getChildren(); if (children.size() == 1 && !(children.get(0).getValue() instanceof StaticArgument)) { // The value has to be a variable final Node> child = children.get(0); // START: Compound arguments /* When we get in here, we need to treat compound arguments a little differently */ if (child.getValue() instanceof CompoundArgument) { @SuppressWarnings("unchecked") final CompoundArgument compoundArgument = (CompoundArgument) child .getValue(); /* See how many arguments it requires */ final int requiredArguments = compoundArgument.getParserTuple().getSize(); /* Figure out whether we even need to care about this */ if (commandQueue.size() <= requiredArguments) { /* Attempt to pop as many arguments from the stack as possible */ for (int i = 0; i < requiredArguments - 1 && commandQueue.size() > 1; i++) { commandQueue.remove(); commandContext.store("__parsing_argument__", i + 2); } } } // END: Compound arguments // START: Flags if (child.getValue() instanceof FlagArgument) { /* Remove all but last */ while (commandQueue.size() > 1) { commandContext.store(FlagArgument.FLAG_META, commandQueue.remove()); } } // END: Flags // START: Array arguments if (child.getValue() != null && GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) { while (commandQueue.size() > 1) { commandQueue.remove(); } } // END: Array arguments if (child.getValue() != null) { if (commandQueue.isEmpty()) { return Collections.emptyList(); // return child.getValue().getParser().suggestions(commandContext, ""); } else if (child.isLeaf() && commandQueue.size() < 2) { return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.peek()); } else if (child.isLeaf()) { if (child.getValue() instanceof CompoundArgument) { final String last = ((LinkedList) commandQueue).getLast(); return child.getValue().getSuggestionsProvider().apply(commandContext, last); } return Collections.emptyList(); } else if (commandQueue.peek().isEmpty()) { return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.remove()); } // START: Preprocessing final ArgumentParseResult preParseResult = child.getValue().preprocess( commandContext, commandQueue ); if (preParseResult.getFailure().isPresent() || !preParseResult.getParsedValue().orElse(false)) { final String value = commandQueue.peek() == null ? "" : commandQueue.peek(); return child.getValue().getSuggestionsProvider().apply(commandContext, value); } // END: Preprocessing // START: Parsing final ArgumentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); if (result.getParsedValue().isPresent()) { commandContext.store(child.getValue().getName(), result.getParsedValue().get()); return this.getSuggestions(commandContext, commandQueue, child); } else if (result.getFailure().isPresent()) { final String value = commandQueue.peek() == null ? "" : commandQueue.peek(); return child.getValue().getSuggestionsProvider().apply(commandContext, value); } // END: Parsing } } /* There are 0 or more static arguments as children. No variable child arguments are present */ if (children.isEmpty() || commandQueue.isEmpty()) { return Collections.emptyList(); } else { final Iterator>> childIterator = root.getChildren().iterator(); if (childIterator.hasNext()) { while (childIterator.hasNext()) { final Node> child = childIterator.next(); if (child.getValue() != null) { final ArgumentParseResult result = child.getValue().getParser().parse( commandContext, commandQueue ); if (result.getParsedValue().isPresent()) { return this.getSuggestions(commandContext, commandQueue, child); } } } } final List suggestions = new LinkedList<>(); for (final Node> argument : root.getChildren()) { if (argument.getValue() == null || this.isPermitted(commandContext.getSender(), argument) != null) { continue; } suggestions.addAll(argument.getValue().getSuggestionsProvider() .apply(commandContext, stringOrEmpty(commandQueue.peek()))); } return suggestions; } } private @NonNull String stringOrEmpty(final @Nullable String string) { if (string == null) { return ""; } return string; } /** * Insert a new command into the command tree * * @param command Command to insert */ @SuppressWarnings("unchecked") public void insertCommand(final @NonNull Command command) { synchronized (this.commandLock) { Node> node = this.internalTree; for (final CommandArgument argument : command.getArguments()) { Node> tempNode = node.getChild(argument); if (tempNode == null) { tempNode = node.addChild(argument); } else if (argument instanceof StaticArgument && tempNode.getValue() != null) { for (final String alias : ((StaticArgument) argument).getAliases()) { ((StaticArgument) tempNode.getValue()).registerAlias(alias); } } if (node.children.size() > 0) { node.children.sort(Comparator.comparing(Node::getValue)); } tempNode.setParent(node); node = tempNode; } if (node.getValue() != null) { if (node.getValue().getOwningCommand() != null) { throw new IllegalStateException(String.format( "Duplicate command chains detected. Node '%s' already has an owning command (%s)", node.toString(), node.getValue().getOwningCommand().toString() )); } node.getValue().setOwningCommand(command); } // Verify the command structure every time we add a new command this.verifyAndRegister(); } } private @Nullable CommandPermission isPermitted( final @NonNull C sender, final @NonNull Node<@Nullable CommandArgument> node ) { final CommandPermission permission = (CommandPermission) node.nodeMeta.get("permission"); if (permission != null) { return this.commandManager.hasPermission(sender, permission) ? null : permission; } if (node.isLeaf()) { return this.commandManager.hasPermission( sender, Objects.requireNonNull( Objects.requireNonNull( node.value, "node.value" ).getOwningCommand(), "owning command" ).getCommandPermission() ) ? null : Objects.requireNonNull(node.value.getOwningCommand(), "owning command") .getCommandPermission(); } /* if any of the children would permit the execution, then the sender has a valid chain to execute, and so we allow them to execute the root */ final List missingPermissions = new LinkedList<>(); for (final Node> child : node.getChildren()) { final CommandPermission check = this.isPermitted(sender, child); if (check == null) { return null; } else { missingPermissions.add(check); } } return OrPermission.of(missingPermissions); } /** * Go through all commands and register them, and verify the * command tree contracts */ public void verifyAndRegister() { // All top level commands are supposed to be registered in the command manager this.internalTree.children.stream().map(Node::getValue).forEach(commandArgument -> { if (!(commandArgument instanceof StaticArgument)) { throw new IllegalStateException("Top level command argument cannot be a variable"); } }); this.checkAmbiguity(this.internalTree); // Verify that all leaf nodes have command registered this.getLeaves(this.internalTree).forEach(leaf -> { if (leaf.getOwningCommand() == null) { throw new NoCommandInLeafException(leaf); } else { final Command owningCommand = leaf.getOwningCommand(); this.commandManager.getCommandRegistrationHandler().registerCommand(owningCommand); } }); // Register command permissions this.getLeavesRaw(this.internalTree).forEach(node -> { // noinspection all final CommandPermission commandPermission = node.getValue().getOwningCommand().getCommandPermission(); /* All leaves must necessarily have an owning command */ node.nodeMeta.put("permission", commandPermission); // Get chain and order it tail->head then skip the tail (leaf node) List>> chain = this.getChain(node); Collections.reverse(chain); chain = chain.subList(1, chain.size()); // Go through all nodes from the tail upwards until a collision occurs for (final Node> commandArgumentNode : chain) { final CommandPermission existingPermission = (CommandPermission) commandArgumentNode.nodeMeta .get("permission"); CommandPermission permission; if (existingPermission != null) { permission = OrPermission.of(Arrays.asList(commandPermission, existingPermission)); } else { permission = commandPermission; } /* Now also check if there's a command handler attached to an upper level node */ if (commandArgumentNode.getValue() != null && commandArgumentNode .getValue() .getOwningCommand() != null) { final Command command = commandArgumentNode.getValue().getOwningCommand(); if (this .getCommandManager() .getSetting(CommandManager.ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS)) { permission = command.getCommandPermission(); } else { permission = OrPermission.of(Arrays.asList(permission, command.getCommandPermission())); } } commandArgumentNode.nodeMeta.put("permission", permission); } }); } private void checkAmbiguity(final @NonNull Node<@Nullable CommandArgument> node) throws AmbiguousNodeException { if (node.isLeaf()) { return; } final int size = node.children.size(); for (final Node> child : node.children) { if (child.getValue() != null && !child.getValue().isRequired() && size > 1) { throw new AmbiguousNodeException( node.getValue(), child.getValue(), node.getChildren() .stream() .filter(n -> n.getValue() != null) .map(Node::getValue).collect(Collectors.toList()) ); } } node.children.forEach(this::checkAmbiguity); } private @NonNull List<@NonNull Node<@Nullable CommandArgument>> getLeavesRaw( final @NonNull Node<@Nullable CommandArgument> node ) { final List>> leaves = new LinkedList<>(); if (node.isLeaf()) { if (node.getValue() != null) { leaves.add(node); } } else { node.children.forEach(child -> leaves.addAll(getLeavesRaw(child))); } return leaves; } private @NonNull List<@NonNull CommandArgument> getLeaves( final @NonNull Node<@NonNull CommandArgument> node ) { final List> leaves = new LinkedList<>(); if (node.isLeaf()) { if (node.getValue() != null) { leaves.add(node.getValue()); } } else { node.children.forEach(child -> leaves.addAll(getLeaves(child))); } return leaves; } private @NonNull List<@NonNull Node<@Nullable CommandArgument>> getChain( final @Nullable Node<@Nullable CommandArgument> end ) { final List>> chain = new LinkedList<>(); Node> tail = end; while (tail != null) { chain.add(tail); tail = tail.getParent(); } Collections.reverse(chain); return chain; } private @Nullable Command cast(final @Nullable Command command) { return command; } /** * Get an immutable collection containing all of the root nodes * in the tree * * @return Root nodes */ public @NonNull Collection<@NonNull Node<@Nullable CommandArgument>> getRootNodes() { return this.internalTree.getChildren(); } /** * Get a named root node, if it exists * * @param name Root node name * @return Root node, or {@code null} */ public @Nullable Node<@Nullable CommandArgument> getNamedNode(final @Nullable String name) { for (final Node> node : this.getRootNodes()) { if (node.getValue() != null && node.getValue() instanceof StaticArgument) { @SuppressWarnings("unchecked") final StaticArgument staticArgument = (StaticArgument) node.getValue(); for (final String alias : staticArgument.getAliases()) { if (alias.equalsIgnoreCase(name)) { return node; } } } } return null; } /** * Get the command manager * * @return Command manager */ public @NonNull CommandManager getCommandManager() { return this.commandManager; } /** * Very simple tree structure * * @param Node value type */ public static final class Node { private final Map nodeMeta = new HashMap<>(); private final List> children = new LinkedList<>(); private final T value; private Node parent; private Node(final @Nullable T value) { this.value = value; } /** * Get an immutable copy of the node's child list * * @return Children */ public @NonNull List<@NonNull Node<@Nullable T>> getChildren() { return Collections.unmodifiableList(this.children); } private @NonNull Node<@Nullable T> addChild(final @NonNull T child) { final Node node = new Node<>(child); this.children.add(node); return node; } private @Nullable Node<@Nullable T> getChild(final @NonNull T type) { for (final Node child : this.children) { if (type.equals(child.getValue())) { return child; } } return null; } /** * Check if the node is a leaf node * * @return {@code true} if the node is a leaf node, else {@code false} */ public boolean isLeaf() { return this.children.isEmpty(); } /** * Get the node meta instance * * @return Node meta */ public @NonNull Map<@NonNull String, @NonNull Object> getNodeMeta() { return this.nodeMeta; } /** * Get the node value * * @return Node value */ public @Nullable T getValue() { return this.value; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final Node node = (Node) o; return Objects.equals(getValue(), node.getValue()); } @Override public int hashCode() { return Objects.hash(getValue()); } /** * Get the parent node * * @return Parent node */ public @Nullable Node<@Nullable T> getParent() { return this.parent; } /** * Set the parent node * * @param parent new parent node */ public void setParent(final @Nullable Node<@Nullable T> parent) { this.parent = parent; } @Override public String toString() { return "Node{value=" + value + '}'; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy