/****************************************************************************** * Copyright (C) 2015 Yevgeny Krasik * * * * 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 com.github.ykrasik.jaci.cli.hierarchy; import com.github.ykrasik.jaci.cli.CliConstants; import com.github.ykrasik.jaci.cli.assist.AutoComplete; import com.github.ykrasik.jaci.cli.command.CliCommand; import com.github.ykrasik.jaci.cli.directory.CliDirectory; import com.github.ykrasik.jaci.cli.exception.ParseError; import com.github.ykrasik.jaci.cli.exception.ParseException; import com.github.ykrasik.jaci.directory.CommandDirectoryDef; import com.github.ykrasik.jaci.hierarchy.CommandHierarchyDef; import com.github.ykrasik.jaci.path.ParsedPath; import com.github.ykrasik.jaci.util.opt.Opt; import java.util.Objects; /** * An implementation of a {@link CliCommandHierarchy}.<br> * Supports 2 types of commands - local commands which must belong to some {@link CliDirectory} * and system commands, which don't belong to any {@link CliDirectory} and are accessible from anywhere, no matter what * the current working directory is. * * @author Yevgeny Krasik */ public class CliCommandHierarchyImpl implements CliCommandHierarchy { /*** * Root directory. */ private final CliDirectory root; /** * Contains system commands that are not associated with any specific directory (stuff like 'cd', 'ls' etc). * These commands only come into play in certain situations (the commandLine must not start with a '/'), but other * then that they are identical to regular commands. For this purpose, it's convenient to store them in a 'virtual' directory. */ private final CliDirectory systemCommands; /** * Current working directory. */ private CliDirectory workingDirectory; private CliCommandHierarchyImpl(CliDirectory root, CliDirectory systemCommands) { this.root = Objects.requireNonNull(root, "root"); this.systemCommands = Objects.requireNonNull(systemCommands, "systemCommands"); this.workingDirectory = root; } @Override public CliDirectory getWorkingDirectory() { return workingDirectory; } @Override public void setWorkingDirectory(CliDirectory workingDirectory) { this.workingDirectory = Objects.requireNonNull(workingDirectory, "workingDirectory"); } @Override public CliDirectory parsePathToDirectory(String rawPath) throws ParseException { // Parse all elements as directories. final ParsedPath path = parsePath(rawPath, false); return parsePathToDirectory(path); } @Override public CliCommand parsePathToCommand(String rawPath) throws ParseException { final ParsedPath path = parsePath(rawPath, true); // TODO: There has to be better way for testing eligibility for being a system command. if (!path.containsDelimiter()) { // path does not contain a '/' delimiter. // It could either be a systemCommands command, or a command under the current workingDirectory. // The last path element is the only path element is this path in this case. return getSystemOrWorkingDirectoryCommand(path.getLastElement()); } // Path contains a '/' delimiter. // Parse the path until the last element as a path to a directory, and have the last directory parse the last element as a command. // So in "path/to/command", parse "path/to" as path to directory "to", and let "to" parse "command". final CliDirectory lastDirectory = parsePathToLastDirectory(path); final String commandName = path.getLastElement(); if (commandName.isEmpty()) { throw new ParseException(ParseError.INVALID_COMMAND, "Path doesn't point to command: '"+rawPath+'\''); } final Opt<CliCommand> command = lastDirectory.getCommand(commandName); if (!command.isPresent()) { throw new ParseException(ParseError.INVALID_COMMAND, "Directory '"+lastDirectory.getName()+"' doesn't contain command: '"+commandName+'\''); } return command.get(); } private CliCommand getSystemOrWorkingDirectoryCommand(String name) throws ParseException { // If 'name' is a system command, return it. final Opt<CliCommand> systemCommand = systemCommands.getCommand(name); if (systemCommand.isPresent()) { return systemCommand.get(); } // 'name' is not a system command, check if it is a child of the current workingDirectory. final Opt<CliCommand> command = workingDirectory.getCommand(name); if (command.isPresent()) { return command.get(); } throw new ParseException(ParseError.INVALID_COMMAND, '\''+name+"' is not a recognized command!"); } @Override public AutoComplete autoCompletePathToDirectory(String rawPath) throws ParseException { final ParsedPath path = parsePath(rawPath, true); // Parse the path until the last element as a path to a directory, // and have the last directory auto complete the last element as a directory. final CliDirectory lastDirectory = parsePathToLastDirectory(path); final String directoryNamePrefix = path.getLastElement(); return lastDirectory.autoCompleteDirectory(directoryNamePrefix); } @Override public AutoComplete autoCompletePath(String rawPath) throws ParseException { final ParsedPath path = parsePath(rawPath, true); final String prefix = path.getLastElement(); // TODO: There has to be better way for testing eligibility for being a system command. if (!path.containsDelimiter()) { // Path does not contain a '/' delimiter. // It could be either a system command or an entry from the current workingDirectory. final AutoComplete systemCommandsAutoComplete = systemCommands.autoCompleteCommand(prefix); final AutoComplete entriesAutoComplete = workingDirectory.autoCompleteEntry(prefix); return systemCommandsAutoComplete.union(entriesAutoComplete); } // Parse the path until the last element as a path to a directory, // and have the last directory auto complete the last element as a directory or command. final CliDirectory lastDirectory = parsePathToLastDirectory(path); return lastDirectory.autoCompleteEntry(prefix); } private ParsedPath parsePath(String path, boolean entry) throws ParseException { try { if (entry) { return ParsedPath.toEntry(path); } else { return ParsedPath.toDirectory(path); } } catch (IllegalArgumentException e) { throw new ParseException(ParseError.INVALID_DIRECTORY, e.getMessage()); } } private CliDirectory parsePathToLastDirectory(ParsedPath path) throws ParseException { final ParsedPath pathToLastDirectory = path.withoutLastElement(); return parsePathToDirectory(pathToLastDirectory); } private CliDirectory parsePathToDirectory(ParsedPath path) throws ParseException { // If the path starts with '/', it starts from root. CliDirectory currentDirectory = path.startsWithDelimiter() ? root : workingDirectory; for (String directoryName : path) { currentDirectory = parseChildDirectory(currentDirectory, directoryName); } return currentDirectory; } private CliDirectory parseChildDirectory(CliDirectory currentDirectory, String name) throws ParseException { if (CliConstants.PATH_THIS.equals(name)) { return currentDirectory; } if (CliConstants.PATH_PARENT.equals(name)) { final Opt<CliDirectory> parent = currentDirectory.getParent(); if (!parent.isPresent()) { throw new ParseException(ParseError.INVALID_DIRECTORY, "Directory '"+currentDirectory.getName()+"' doesn't have a parent."); } return parent.get(); } final Opt<CliDirectory> childDirectory = currentDirectory.getDirectory(name); if (!childDirectory.isPresent()) { throw new ParseException(ParseError.INVALID_DIRECTORY, "Directory '"+currentDirectory.getName()+"' doesn't contain directory: '"+name+'\''); } return childDirectory.get(); } /** * Construct a CLI hierarchy from a {@link CommandHierarchyDef}. * * @param def CommandHierarchyDef to construct a CLI hierarchy from. * @return A CLI hierarchy constructed from the CommandHierarchyDef. */ public static CliCommandHierarchyImpl from(CommandHierarchyDef def) { // Create hierarchy with the parameter as the root. final CommandDirectoryDef rootDef = def.getRoot(); final CliDirectory root = CliDirectory.fromDef(rootDef); // Create system commands 'virtual' directory. // System commands need to operate on an already built hierarchy, but... we are exactly in the process of building one. // In order to fully build a hierarchy, we must provide a set of system commands. // This is a cyclic dependency - resolved through the use of a 'promise' object, which will delegate all calls to the // concrete hierarchy, once it's built. final CliCommandHierarchyPromise hierarchyPromise = new CliCommandHierarchyPromise(); final CliDirectory systemCommands = CliSystemCommandFactory.from(hierarchyPromise); // Update the 'promise' hierarchy with the concrete implementation. final CliCommandHierarchyImpl cliHierarchy = new CliCommandHierarchyImpl(root, systemCommands); hierarchyPromise.setDelegate(cliHierarchy); return cliHierarchy; } /** * A {@link CliCommandHierarchy} that promises to <b>eventually</b> contain a concrete implementation of a {@link CliCommandHierarchy}. * Required in order to resolve dependency issues between system commands and the immutability of {@link CliCommandHierarchyImpl}. * System commands require a concrete {@link CliCommandHierarchy} to operate on, but {@link CliCommandHierarchyImpl} requires * all system commands to be available at construction time - cyclic dependency. * So this class was born as a compromise. */ private static class CliCommandHierarchyPromise implements CliCommandHierarchy { private CliCommandHierarchy delegate; public void setDelegate(CliCommandHierarchy delegate) { this.delegate = delegate; } @Override public CliDirectory getWorkingDirectory() { return delegate.getWorkingDirectory(); } @Override public void setWorkingDirectory(CliDirectory workingDirectory) { delegate.setWorkingDirectory(workingDirectory); } @Override public CliDirectory parsePathToDirectory(String rawPath) throws ParseException { return delegate.parsePathToDirectory(rawPath); } @Override public CliCommand parsePathToCommand(String rawPath) throws ParseException { return delegate.parsePathToCommand(rawPath); } @Override public AutoComplete autoCompletePathToDirectory(String rawPath) throws ParseException { return delegate.autoCompletePathToDirectory(rawPath); } @Override public AutoComplete autoCompletePath(String rawPath) throws ParseException { return delegate.autoCompletePath(rawPath); } } }