Skip to content

Commit

Permalink
Merge pull request #485 from gastaldi/FORGE-1802
Browse files Browse the repository at this point in the history
FORGE-1802: Introduced CommandNotFoundListener
  • Loading branch information
lincolnthree committed Jul 9, 2014
2 parents 846d82b + 2b401b9 commit 22c0a48
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 170 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Eclipse Public License version 1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.jboss.forge.addon.shell;

import org.jboss.forge.addon.ui.context.UIContext;

/**
* Listens for missing commands in {@link Shell}
*
* @author <a href="ggastald@redhat.com">George Gastaldi</a>
*/
public interface CommandNotFoundListener
{
/**
* Called when the command to be executed is not found
*/
void onCommandNotFound(String line, UIContext context);

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ public interface Shell extends UIProvider, AutoCloseable
* be removed.
*/
ListenerRegistration<CommandExecutionListener> addCommandExecutionListener(CommandExecutionListener listener);

/**
* Add a {@link CommandNotFoundListener}, returning the {@link ListenerRegistration} with which it may subsequently
* be removed.
*/
ListenerRegistration<CommandNotFoundListener> addCommandNotFoundListener(CommandNotFoundListener listener);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.jboss.forge.addon.resource.Resource;
import org.jboss.forge.addon.shell.aesh.ForgeCommandNotFoundHandler;
import org.jboss.forge.addon.shell.aesh.ForgeCommandRegistry;
import org.jboss.forge.addon.shell.ui.DidYouMeanCommandNotFoundListener;
import org.jboss.forge.addon.shell.ui.ShellContext;
import org.jboss.forge.addon.shell.ui.ShellContextImpl;
import org.jboss.forge.addon.shell.ui.ShellUIOutputImpl;
Expand Down Expand Up @@ -73,6 +74,7 @@ public class ShellImpl implements Shell, UIRuntime
private final AeshConsole console;
private final UIOutput output;
private final List<CommandExecutionListener> executionListeners = new LinkedList<>();
private final List<CommandNotFoundListener> commandNotFoundListeners = new LinkedList<>();

private final static Logger log = Logger.getLogger(ShellImpl.class.getName());

Expand All @@ -87,6 +89,8 @@ public ShellImpl(Resource<?> initialResource, Settings settings, AddonRegistry a
File export = new File(forgeHome, "export");
final ForgeCommandRegistry registry =
new ForgeCommandRegistry(this, addonRegistry);
// Register DidYouMeanListener
commandNotFoundListeners.add(new DidYouMeanCommandNotFoundListener(registry));
SettingsBuilder newSettings = new SettingsBuilder(settings)
.historyFile(history)
.aliasFile(alias)
Expand All @@ -103,7 +107,7 @@ public ShellImpl(Resource<?> initialResource, Settings settings, AddonRegistry a
.prompt(createPrompt(initialResource))
.settings(newSettings.create())
.commandRegistry(registry)
.commandNotFoundHandler(new ForgeCommandNotFoundHandler(registry))
.commandNotFoundHandler(new ForgeCommandNotFoundHandler(this, commandNotFoundListeners))
.create();
this.output = new ShellUIOutputImpl(console);
setCurrentResource(initialResource);
Expand Down Expand Up @@ -243,6 +247,22 @@ public CommandExecutionListener removeListener()
};
}

@Override
public ListenerRegistration<CommandNotFoundListener> addCommandNotFoundListener(
final CommandNotFoundListener listener)
{
commandNotFoundListeners.add(listener);
return new ListenerRegistration<CommandNotFoundListener>()
{
@Override
public CommandNotFoundListener removeListener()
{
commandNotFoundListeners.remove(listener);
return listener;
}
};
}

@Override
public UIProgressMonitor createProgressMonitor(UIContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,191 +7,39 @@

package org.jboss.forge.addon.shell.aesh;

import java.util.Set;
import java.util.TreeSet;
import java.util.List;

import org.jboss.aesh.console.command.registry.CommandRegistry;
import org.jboss.aesh.console.settings.CommandNotFoundHandler;
import org.jboss.aesh.terminal.Color;
import org.jboss.aesh.terminal.Color.Intensity;
import org.jboss.aesh.terminal.Shell;
import org.jboss.aesh.terminal.TerminalColor;
import org.jboss.aesh.terminal.TerminalString;
import org.jboss.forge.addon.shell.ShellMessages;
import org.jboss.forge.addon.shell.CommandNotFoundListener;
import org.jboss.forge.addon.shell.ShellImpl;
import org.jboss.forge.addon.shell.ui.ShellContextImpl;

/**
*
* @author <a href="ggastald@redhat.com">George Gastaldi</a>
*/
public class ForgeCommandNotFoundHandler implements CommandNotFoundHandler
{
private static final int LETTERS_NEEDED_TO_BE_REPLACED = 2;
private final List<CommandNotFoundListener> listeners;
private final ShellImpl shellImpl;

private final CommandRegistry commandRegistry;

public ForgeCommandNotFoundHandler(CommandRegistry commandRegistry)
public ForgeCommandNotFoundHandler(ShellImpl shell, List<CommandNotFoundListener> listeners)
{
this.commandRegistry = commandRegistry;
super();
this.shellImpl = shell;
this.listeners = listeners;
}

@Override
public void handleCommandNotFound(String line, Shell shell)
{
String commandName = line.split(" ")[0];
// Find similar plugins
Set<String> similarPlugins = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (String command : commandRegistry.getAllCommandNames())
{
if (getLevenshteinDistance(commandName, command) < LETTERS_NEEDED_TO_BE_REPLACED)
{
similarPlugins.add(command);
}
}

ShellMessages.error(shell.err(), "No such command: " + commandName);
if (!similarPlugins.isEmpty())
{
shell.out().println();
if (similarPlugins.size() == 1)
{
shell.out().println("Did you mean this?");
}
else
{
shell.out().println("Did you mean any of these?");
}
for (String plugin : similarPlugins)
{
shell.out().println(
new TerminalString("\t" + plugin,
new TerminalColor(Color.DEFAULT, Color.DEFAULT, Intensity.BRIGHT)));
}
}

}

/**
* <p>
* Find the Levenshtein distance between two Strings.
* </p>
*
* <p>
* This is the number of changes needed to change one String into another, where each change is a single character
* modification (deletion, insertion or substitution).
* </p>
*
* <p>
* The previous implementation of the Levenshtein distance algorithm was from <a
* href="http://www.merriampark.com/ld.htm">http://www.merriampark.com/ld.htm</a>
* </p>
*
* <p>
* Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError which can occur when my Java
* implementation is used with very large strings.<br>
* This implementation of the Levenshtein distance algorithm is from <a
* href="http://www.merriampark.com/ldjava.htm">http://www.merriampark.com/ldjava.htm</a>
* </p>
*
* <pre>
* StringUtils.getLevenshteinDistance(null, *) = IllegalArgumentException
* StringUtils.getLevenshteinDistance(*, null) = IllegalArgumentException
* StringUtils.getLevenshteinDistance("","") = 0
* StringUtils.getLevenshteinDistance("","a") = 1
* StringUtils.getLevenshteinDistance("aaapppp", "") = 7
* StringUtils.getLevenshteinDistance("frog", "fog") = 1
* StringUtils.getLevenshteinDistance("fly", "ant") = 3
* StringUtils.getLevenshteinDistance("elephant", "hippo") = 7
* StringUtils.getLevenshteinDistance("hippo", "elephant") = 7
* StringUtils.getLevenshteinDistance("hippo", "zzzzzzzz") = 8
* StringUtils.getLevenshteinDistance("hello", "hallo") = 1
* </pre>
*
* @param s the first String, must not be null
* @param t the second String, must not be null
* @return result distance
* @throws IllegalArgumentException if either String input {@code null}
*/
public static int getLevenshteinDistance(CharSequence s, CharSequence t)
{
if (s == null || t == null)
{
throw new IllegalArgumentException("Strings must not be null");
}

/*
* The difference between this impl. and the previous is that, rather than creating and retaining a matrix of size
* s.length() + 1 by t.length() + 1, we maintain two single-dimensional arrays of length s.length() + 1. The
* first, d, is the 'current working' distance array that maintains the newest distance cost counts as we iterate
* through the characters of String s. Each time we increment the index of String t we are comparing, d is copied
* to p, the second int[]. Doing so allows us to retain the previous cost counts as required by the algorithm
* (taking the minimum of the cost count to the left, up one, and diagonally up and to the left of the current
* cost count being calculated). (Note that the arrays aren't really copied anymore, just switched...this is
* clearly much better than cloning an array or doing a System.arraycopy() each time through the outer loop.)
*
* Effectively, the difference between the two implementations is this one does not cause an out of memory
* condition when calculating the LD over two very large strings.
*/

int n = s.length(); // length of s
int m = t.length(); // length of t

if (n == 0)
{
return m;
}
else if (m == 0)
try (ShellContextImpl uiContext = shellImpl.createUIContext())
{
return n;
}

if (n > m)
{
// swap the input strings to consume less memory
CharSequence tmp = s;
s = t;
t = tmp;
n = m;
m = t.length();
}

int p[] = new int[n + 1]; // 'previous' cost array, horizontally
int d[] = new int[n + 1]; // cost array, horizontally
int _d[]; // placeholder to assist in swapping p and d

// indexes into strings s and t
int i; // iterates through s
int j; // iterates through t

char t_j; // jth character of t

int cost; // cost

for (i = 0; i <= n; i++)
{
p[i] = i;
}

for (j = 1; j <= m; j++)
{
t_j = t.charAt(j - 1);
d[0] = j;

for (i = 1; i <= n; i++)
for (CommandNotFoundListener listener : listeners)
{
cost = s.charAt(i - 1) == t_j ? 0 : 1;
// minimum of cell to the left+1, to the top+1, diagonally left and up +cost
d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
listener.onCommandNotFound(line, uiContext);
}

// copy current distance counts to 'previous row' distance counts
_d = p;
p = d;
d = _d;
}

// our last action in the above loop was to switch d and p, so p now
// actually has the most recent cost counts
return p[n];
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.jboss.forge.addon.resource.Resource;
import org.jboss.forge.addon.resource.ResourceFactory;
import org.jboss.forge.addon.resource.util.ResourcePathResolver;
import org.jboss.forge.addon.shell.CommandNotFoundListener;
import org.jboss.forge.addon.shell.Shell;
import org.jboss.forge.addon.shell.ShellFactory;
import org.jboss.forge.addon.shell.ui.AbstractShellCommand;
Expand Down Expand Up @@ -248,6 +249,8 @@ public Result execute(Shell shell, BufferedWriter stdin, String line, int quanti
ScriptCommandListener listener = new ScriptCommandListener();
ListenerRegistration<CommandExecutionListener> listenerRegistration = shell
.addCommandExecutionListener(listener);
ListenerRegistration<CommandNotFoundListener> notFoundRegistration = shell
.addCommandNotFoundListener(listener);
try
{
stdin.write(line);
Expand Down Expand Up @@ -277,12 +280,13 @@ public Result execute(Shell shell, BufferedWriter stdin, String line, int quanti
finally
{
listenerRegistration.removeListener();
notFoundRegistration.removeListener();
}
}
return result;
}

public class ScriptCommandListener extends AbstractCommandExecutionListener
public class ScriptCommandListener extends AbstractCommandExecutionListener implements CommandNotFoundListener
{
Result result;

Expand All @@ -309,6 +313,15 @@ public void postCommandFailure(UICommand command, UIExecutionContext context, Th
}
}

@Override
public void onCommandNotFound(String line, UIContext context)
{
synchronized (this)
{
this.result = Results.fail("Command not found: " + line);
}
}

public boolean isExecuted()
{
synchronized (this)
Expand Down

0 comments on commit 22c0a48

Please sign in to comment.