-
Notifications
You must be signed in to change notification settings - Fork 465
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1680 from spyrkob/WFCORE-1657
WFCORE-1657 Confusing tab completion for adding a module dependencies
- Loading branch information
Showing
4 changed files
with
717 additions
and
20 deletions.
There are no files selected for viewing
243 changes: 243 additions & 0 deletions
243
cli/src/main/java/org/jboss/as/cli/handlers/ModuleNameTabCompleter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.