Skip to content

Commit

Permalink
Merge pull request #1680 from spyrkob/WFCORE-1657
Browse files Browse the repository at this point in the history
WFCORE-1657 Confusing tab completion for adding a module dependencies
  • Loading branch information
bstansberry authored Sep 19, 2016
2 parents 4e74eb4 + 54b4569 commit d273699
Show file tree
Hide file tree
Showing 4 changed files with 717 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* JBoss, Home of Professional Open Source
* Copyright 2016, JBoss Inc., and individual contributors as indicated
* by the @authors tag.
*
* 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 org.jboss.as.cli.handlers;

import org.jboss.as.cli.EscapeSelector;
import org.jboss.as.cli.Util;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
* Generates suggestions for module names. Each suggestion generates only next part of the name (ie. up to the name separator).
*
* Assumes the module repository used has standard layered repository layout. Matching suggestions are found:
* <uL>
* <li>under the repository root (excluding 'system' directory)
* <li>under the system/layers/{layer name}
* <li>under the system/add-ons/{add-on name}
* </uL>
* Modules changed, removed or added via patches are not included in the suggestions.
* The modules are not validated - invalid or disabled modules and empty directories are included in the suggestions.
*
* @author Bartosz Spyrko-Smietanko
*/
public class ModuleNameTabCompleter {

private static final EscapeSelector ESCAPE_SELECTOR = ch -> ch == '\\' || ch == ' ' || ch == '"';
private static final String MODULE_NAME_SEPARATOR = ".";
public static final String LAYERS_DIR = "system/layers";
public static final String ADDONS_DIR = "system/add-ons";

private final File modulesRoot;
private final File layersDir;
private final File addonsDir;
private final boolean includeSystemModules;
private final boolean excludeNonModuleFolders;

private ModuleNameTabCompleter(Builder builder) {
modulesRoot = builder.modulesRoot.getAbsoluteFile();
layersDir = new File(modulesRoot, LAYERS_DIR);
addonsDir = new File(modulesRoot, ADDONS_DIR);

this.excludeNonModuleFolders = builder.excludeNonModuleFolders;
this.includeSystemModules = builder.includeSystemModules;
}


public List<String> complete(String buffer) {
final String userEntry = buffer == null ? "" : buffer;
final Set<String> suggestions = new TreeSet<>(); // TreeSet deals with duplication and ordering

List<File> moduleTrees = findInitialModuleDirectories();

moduleTrees.forEach(f -> findSuggestion(f, f.getName(), userEntry, suggestions));

return new ArrayList<>(suggestions);
}

private List<File> findInitialModuleDirectories() {
List<File> moduleTrees = new ArrayList<>();

moduleTrees.addAll(Arrays.asList(modulesRoot.listFiles(this::isNotSystemFolder)));

if (includeSystemModules && layersDir.exists()) {
for (File layer : layersDir.listFiles(File::isDirectory)) {
moduleTrees.addAll(Arrays.asList(layer.listFiles(this::isNotPatchFolder)));
}
}

if (includeSystemModules && addonsDir.exists()) {
for (File addon : addonsDir.listFiles(File::isDirectory)) {
moduleTrees.addAll(Arrays.asList(addon.listFiles(this::isNotPatchFolder)));
}
}

return moduleTrees;
}

private void findSuggestion(File currentDirectory, String suggestion, String userEntry, Collection<String> candidates) {
if (!matchesUserEntry(currentDirectory, userEntry) || (excludeNonModuleFolders && isSlotDirectory(currentDirectory))) {
return;
}

if (tail(userEntry).isEmpty() && !requestsSubmodules(userEntry)) {
final String fullModuleName = Util.escapeString(suggestion, ESCAPE_SELECTOR);
final String partialModuleName = Util.escapeString(suggestion + MODULE_NAME_SEPARATOR, ESCAPE_SELECTOR);

if (excludeNonModuleFolders) {
final boolean isExactMatch = currentDirectory.getName().equals(userEntry);
final boolean hasNestedModules = hasNestedModules(currentDirectory);
final boolean isCompleteModule = isCompleteModule(currentDirectory);


/*
The suggestion should have a trailing separator ('.') if it's a part of longer module name.
If the suggested name is both a full module name and a part of longer name (ie. nested modules), suggest
the name without separator - unless user input is a complete name in which case suggest both options.
*/
if (isCompleteModule && hasNestedModules && isExactMatch) {
candidates.add(fullModuleName);
candidates.add(partialModuleName);
} else if (isCompleteModule) {
candidates.add(fullModuleName);
} else if (hasNestedModules) {
candidates.add(partialModuleName);
}
} else {
final boolean hasChildren = currentDirectory.listFiles(File::isDirectory).length > 0;
final boolean isExactMatch = currentDirectory.getName().equals(userEntry);

if (hasChildren && isExactMatch) {
candidates.add(partialModuleName);
}
candidates.add(fullModuleName);
}
} else {
for (File file : currentDirectory.listFiles(File::isDirectory)) {
findSuggestion(file, suggestion + MODULE_NAME_SEPARATOR + file.getName(), tail(userEntry), candidates);
}
}
}

private boolean matchesUserEntry(File currentDirectory, String userEntry) {
if (!userEntry.endsWith(MODULE_NAME_SEPARATOR) && tail(userEntry).isEmpty()) {
return currentDirectory.getName().startsWith(head(userEntry));
} else {
return currentDirectory.getName().equals(head(userEntry));
}
}

private boolean isCompleteModule(File file) {
return file.listFiles(f -> f.isDirectory() && isSlotDirectory(f)).length > 0;
}


private boolean hasNestedModules(File file) {
final File[] nonSlotChildren = file.listFiles(f -> f.isDirectory() && !isSlotDirectory(f));
for (File potentialModule : nonSlotChildren) {
if (subModuleExists(potentialModule)) {
return true;
}
}
return false;
}

// depth- first search for any module - just to check that the suggestion has any chance of delivering correct result
private boolean subModuleExists(File dir) {
if (isSlotDirectory(dir)) {
return true;
} else {
File[] children = dir.listFiles(File::isDirectory);
for (File child : children) {
if (subModuleExists(child)) {
return true;
}
}
}

return false;
}

private boolean isSlotDirectory(File currentDirectory) {
return currentDirectory.listFiles(f -> f.getName().equals("module.xml")).length > 0;
}

private boolean requestsSubmodules(String moduleNamePattern) {
return moduleNamePattern.endsWith(MODULE_NAME_SEPARATOR);
}

private boolean isNotSystemFolder(File f) {
return f.isDirectory() && !f.getName().equals("system");
}

private boolean isNotPatchFolder(File f) {
return f.isDirectory() && !f.getName().equals("patches");
}

// get first part of module name (up to separator)
private String head(String moduleName) {
if (moduleName.indexOf(MODULE_NAME_SEPARATOR) > 0) {
return moduleName.substring(0, moduleName.indexOf(MODULE_NAME_SEPARATOR));
} else {
return moduleName;
}
}

// get all parts of module name apart from first
private String tail(String moduleName) {
if (moduleName.indexOf(MODULE_NAME_SEPARATOR) > 0) {
return moduleName.substring(moduleName.indexOf(MODULE_NAME_SEPARATOR) + 1);
} else {
return "";
}
}

public static Builder completer(File modulesRoot) {
return new Builder(modulesRoot);
}

public static class Builder {
private final File modulesRoot;
private boolean includeSystemModules;
private boolean excludeNonModuleFolders;

public Builder(File modulesRoot) {
this.modulesRoot = modulesRoot;
}

public Builder includeSystemModules(boolean includeSystemModules) {
this.includeSystemModules = includeSystemModules;
return this;
}

public Builder excludeNonModuleFolders(boolean excludeNonModuleFolders) {
this.excludeNonModuleFolders = excludeNonModuleFolders;
return this;
}

public ModuleNameTabCompleter build() {
return new ModuleNameTabCompleter(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.jboss.as.cli.CommandLineCompleter;
import org.jboss.as.cli.CommandLineException;
import org.jboss.as.cli.Util;
import org.jboss.as.cli.handlers.ModuleNameTabCompleter;
import org.jboss.as.cli.handlers.CommandHandlerWithHelp;
import org.jboss.as.cli.handlers.DefaultFilenameTabCompleter;
import org.jboss.as.cli.handlers.FilenameTabCompleter;
Expand Down Expand Up @@ -128,30 +129,27 @@ public ASModuleHandler(CommandContext ctx) {
super("module", false);

final FilenameTabCompleter pathCompleter = Util.isWindows() ? new WindowsFilenameTabCompleter(ctx) : new DefaultFilenameTabCompleter(ctx);
final CommandLineCompleter moduleNameCompleter = new CommandLineCompleter() {

moduleRootDir = new FileSystemPathArgument(this, pathCompleter, "--module-root-dir");

name = new ArgumentWithValue(this, new CommandLineCompleter() {
@Override
public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) {
String path = buffer.replace('.', File.separatorChar);
String modulesPath;
try {
modulesPath = getModulesDir(ctx).getAbsolutePath() + File.separatorChar;
String currentAction = action.getValue(ctx.getParsedCommandLine());
// suggest only modules from user's repository, not system modules
final ModuleNameTabCompleter moduleNameCompleter = ModuleNameTabCompleter.completer(getModulesDir(ctx))
.excludeNonModuleFolders(ACTION_REMOVE.equals(currentAction))
.includeSystemModules(ACTION_ADD.equals(currentAction))
.build();

candidates.addAll(moduleNameCompleter.complete(buffer));
return 0;
} catch (CommandLineException e) {
return -1;
}
int result = pathCompleter.complete(ctx, modulesPath + path, cursor, candidates);
if(result < 0) {
return result;
}
for(int i = 0; i < candidates.size(); ++i) {
candidates.set(i, candidates.get(i).replace(File.separatorChar, '.'));
}
return result - modulesPath.length();
}
};

moduleRootDir = new FileSystemPathArgument(this, pathCompleter, "--module-root-dir");

name = new ArgumentWithValue(this, moduleNameCompleter, "--name") {
}, "--name") {
@Override
protected ParsingState initParsingState() {
final ExpressionBaseState state = new ExpressionBaseState("EXPR", true, false);
Expand Down Expand Up @@ -240,10 +238,24 @@ protected ParsingState initParsingState() {
@Override
public int complete(CommandContext ctx, String buffer, int cursor, List<String> candidates) {
final int lastSeparator = buffer.lastIndexOf(MODULE_SEPARATOR);
if(lastSeparator >= 0) {
return lastSeparator + 1 + moduleNameCompleter.complete(ctx, buffer.substring(lastSeparator + 1), cursor, candidates);

try {
// any module (including system) can be a dependency
final ModuleNameTabCompleter moduleNameCompleter = ModuleNameTabCompleter.completer(getModulesDir(ctx))
.excludeNonModuleFolders(true)
.includeSystemModules(true)
.build();

if (lastSeparator >= 0) {
candidates.addAll(moduleNameCompleter.complete(buffer.substring(lastSeparator + 1)));
return lastSeparator + 1;
} else {
candidates.addAll(moduleNameCompleter.complete(buffer));
return 0;
}
} catch (CommandLineException e) {
return -1;
}
return moduleNameCompleter.complete(ctx, buffer, cursor, candidates);
}});
props = new AddModuleListArgument("--properties");

Expand Down
Loading

0 comments on commit d273699

Please sign in to comment.