// Copyright 2012 Georg-August-Universität Göttingen, Germany // // 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 de.ugoe.cs.util.console; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.logging.Level; import de.ugoe.cs.util.StringTools; /** *

* Executes commands. The commands have to implement the {@link Command} interface and be in * packages registered using addCommandPackage(). Additionally, default commands are implemented in * the de.ugoe.cs.util.console.defaultcommands package. *

*

* This class is implemented as a Singleton. *

* * @author Steffen Herbold * @version 1.0 */ public class CommandExecuter { /** *

* Handle of the CommandExecuter instance. *

*/ private final static CommandExecuter theInstance = new CommandExecuter(); /** *

* Prefix of all command classes. *

*/ private static final String cmdPrefix = "CMD"; /** *

* Name of the package for default commands. *

*/ private static final String defaultPackage = "de.ugoe.cs.util.console.defaultcommands"; /** *

* List of packages in which commands may be defined. The exec methods trys to load command from * these packages in the order they have been added. *

*

* The de.ugoe.cs.util.console.defaultcommands package has always lowest priority, unless it is * specifically added. *

*/ private List commandPackageList; /** *

* the list of available commands (lazy instantiation in the method * {@link #getAvailableCommands()}) *

*/ private Command[] availableCommands; /** *

* Returns the instance of CommandExecuter. If no instances exists yet, a new one is created. *

* * @return the instance of CommandExecuter */ public static synchronized CommandExecuter getInstance() { return theInstance; } /** *

* Creates a new CommandExecuter. Private to prevent multiple instances (Singleton). *

*/ private CommandExecuter() { commandPackageList = new ArrayList(); } /** *

* Adds a package that will be used by {@link #exec(String)} to load command from. *

* * @param pkg * package where commands are located * @throws IllegalArgumentException * thrown if the package name is null or empty string */ public void addCommandPackage(String pkg) { if ("".equals(pkg) || pkg == null) { throw new IllegalArgumentException("package name must not be null or empty string"); } commandPackageList.add(pkg); } /** *

* Executes the command defined by string. A command has the following form (mix of EBNF and * natural language): *

* * <command> := <commandname><whitespace>{<parameter>}
* <commandname> := String without whitespaces. Has to be a valid Java class name
* <parameter> := <string>|<stringarray>
* <string> := <stringwithoutwhitespaces>|<stringwithwhitespaces> * <stringwithoutwhitespaces> := a string without whitespaces
* <stringwithoutwhitespaces> := a string, that can have whitespaces, but must be in double quotes
* <stringarray> := "["<string>{<whitespace><string>"]" *
* * @param command * the command as a string */ public void exec(String command) { Console.commandNotification(command); Command cmd = null; CommandParser parser = new CommandParser(); parser.parse(command); for (int i = 0; cmd == null && i < commandPackageList.size(); i++) { cmd = loadCMD(commandPackageList.get(i) + "." + cmdPrefix + parser.getCommandName()); } if (cmd == null) { // check if command is available as default command cmd = loadCMD(defaultPackage + "." + cmdPrefix + parser.getCommandName()); } if (cmd == null) { Console.println("Unknown command"); } else { try { cmd.run(parser.getParameters()); } catch (IllegalArgumentException e) { Console.println("Usage: " + cmd.help()); } } } /** *

* Helper method that loads a class and tries to cast it to {@link Command}. *

* * @param className * qualified name of the class (including package name) * @return if class is available and implement {@link Command} and instance of the class, null * otherwise */ public Command loadCMD(String className) { Command cmd = null; try { Class cmdClass = Class.forName(className); cmd = (Command) cmdClass.newInstance(); } catch (NoClassDefFoundError e) { String[] splitResult = e.getMessage().split("CMD"); String correctName = splitResult[splitResult.length - 1].replace(")", ""); Console.println("Did you mean " + correctName + "?"); } catch (ClassNotFoundException e) {} catch (IllegalAccessException e) {} catch (InstantiationException e) {} catch (ClassCastException e) { Console.traceln(Level.WARNING, className + "found, but does not implement Command"); } return cmd; } /** *

* Helper method that loads a class and tries to cast it to {@link Command}. *

* * @param className * qualified name of the class (including package name) * @return if class is available and implement {@link Command} and instance of the class, null * otherwise */ public Command getCMD(String commandName) { Command cmd = null; for (int i = 0; cmd == null && i < commandPackageList.size(); i++) { cmd = loadCMD(commandPackageList.get(i) + "." + cmdPrefix + commandName); } if (cmd == null) { // check if command is available as default command cmd = loadCMD(defaultPackage + "." + cmdPrefix + commandName); } return cmd; } /** *

* reads all available commands from the registered command packages and returns a list of their * names *

* * @return an array containing the names of the available commands. */ public Command[] getAvailableCommands() { if (availableCommands == null) { List commands = new ArrayList(); List packages = new ArrayList(); packages.addAll(commandPackageList); packages.add(defaultPackage); FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return (name != null) && (name.startsWith(cmdPrefix)) && (name.endsWith(".class")); } }; SortedSet classNames = new TreeSet(new Comparator() { @Override public int compare(String arg1, String arg2) { String str1 = arg1.substring(arg1.lastIndexOf('.') + cmdPrefix.length() + 1); String str2 = arg2.substring(arg2.lastIndexOf('.') + cmdPrefix.length() + 1); return str1.compareTo(str2); } }); for (String packageName : packages) { String path = packageName.replace('.', '/'); try { Enumeration resources = ClassLoader.getSystemResources(path); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); File packageDir = new File(resource.getFile()); if (packageDir.isDirectory()) { for (File classFile : packageDir.listFiles(filter)) { String className = classFile.getName().substring (0, classFile.getName().lastIndexOf('.')); classNames.add(packageName + "." + className); } } else { int index = resource.getFile().lastIndexOf('!'); if ((index > 0) && (resource.getFile().startsWith("file:")) && (resource.getFile().endsWith("!/" + path))) { String jarFile = resource.getFile().substring("file:".length(), index); // we have to read the package content from a jar file JarInputStream jarInputStream = null; try { jarInputStream = new JarInputStream(new FileInputStream(jarFile)); JarEntry entry = null; do { entry = jarInputStream.getNextJarEntry(); if ((entry != null) && (!entry.isDirectory()) && (entry.getName().startsWith(path))) { String className = entry.getName().substring (path.length() + 1, entry.getName().lastIndexOf('.')); classNames.add(packageName + "." + className); } } while (entry != null); } catch (Exception e) { e.printStackTrace(); Console.traceln(Level.WARNING, "could not read contents of " + "jar " + jarFile); } finally { if (jarInputStream != null) { jarInputStream.close(); } } } } } } catch (IOException e) { Console.traceln (Level.WARNING, "could not read commands of package " + packageName); } } for (String className : classNames) { // class may still be inner classes. Therefore load the command, to // see if it is really available and a command. Command cmd = loadCMD(className); if (cmd != null) { commands.add(cmd); } } availableCommands = commands.toArray(new Command[commands.size()]); } return Arrays.copyOf(availableCommands, availableCommands.length); } /** *

* Get a copy of the currently registered command packages. *

* * @return currently registered command packages */ public List getCommandPackages() { List commandPackageListCopy = new ArrayList(commandPackageList); commandPackageListCopy.add(0, defaultPackage); return commandPackageListCopy; } /** *

* this method method performs an auto completion of the provided String as far as possible * regarding the available commands. It auto completes to the full command name, if only * one command matches the given prefix. It auto completes to the common denominator, if * several commands match the prefix *

* * @param commandPrefix the prefix to be auto completed * * @return as described */ public String autoCompleteCommand(String commandPrefix) { Command[] commands = getAvailableCommands(); String[] completions = new String[commands.length]; for (int i = 0; i < commands.length; i++) { completions[i] = commands[i].getClass().getSimpleName().substring(3); } return StringTools.autocomplete(commandPrefix, completions); } }