/*
* 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);
}
}
}