source: trunk/java-utils/src/main/java/de/ugoe/cs/util/console/CommandExecuter.java @ 1238

Last change on this file since 1238 was 1238, checked in by pharms, 11 years ago
  • added autocompletion and history of commands
File size: 15.0 KB
Line 
1//   Copyright 2012 Georg-August-Universität Göttingen, Germany
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
14
15
16package de.ugoe.cs.util.console;
17
18import java.io.File;
19import java.io.FileInputStream;
20import java.io.FilenameFilter;
21import java.io.IOException;
22import java.net.URL;
23import java.util.ArrayList;
24import java.util.Comparator;
25import java.util.Enumeration;
26import java.util.List;
27import java.util.SortedSet;
28import java.util.TreeSet;
29import java.util.jar.JarEntry;
30import java.util.jar.JarInputStream;
31import java.util.logging.Level;
32
33/**
34 * <p>
35 * Executes commands. The commands have to implement the {@link Command} interface and be in
36 * packages registered using addCommandPackage(). Additionally, default commands are implemented in
37 * the de.ugoe.cs.util.console.defaultcommands package.
38 * </p>
39 * <p>
40 * This class is implemented as a <i>Singleton</i>.
41 * </p>
42 *
43 * @author Steffen Herbold
44 * @version 1.0
45 */
46public class CommandExecuter {
47
48    /**
49     * <p>
50     * Handle of the CommandExecuter instance.
51     * </p>
52     */
53    private final static CommandExecuter theInstance = new CommandExecuter();
54
55    /**
56     * <p>
57     * Prefix of all command classes.
58     * </p>
59     */
60    private static final String cmdPrefix = "CMD";
61
62    /**
63     * <p>
64     * Name of the package for default commands.
65     * </p>
66     */
67    private static final String defaultPackage = "de.ugoe.cs.util.console.defaultcommands";
68
69    /**
70     * <p>
71     * List of packages in which commands may be defined. The exec methods trys to load command from
72     * these packages in the order they have been added.
73     * </p>
74     * <p>
75     * The de.ugoe.cs.util.console.defaultcommands package has always lowest priority, unless it is
76     * specifically added.
77     * </p>
78     */
79    private List<String> commandPackageList;
80   
81    /**
82     * <p>
83     * the list of available commands (lazy instantiation in the method
84     * {@link #getAvailableCommands()})
85     * <p>
86     */
87    private Command[] availableCommands;
88
89    /**
90     * <p>
91     * Returns the instance of CommandExecuter. If no instances exists yet, a new one is created.
92     * </p>
93     *
94     * @return the instance of CommandExecuter
95     */
96    public static synchronized CommandExecuter getInstance() {
97        return theInstance;
98    }
99
100    /**
101     * <p>
102     * Creates a new CommandExecuter. Private to prevent multiple instances (Singleton).
103     * </p>
104     */
105    private CommandExecuter() {
106        commandPackageList = new ArrayList<String>();
107    }
108
109    /**
110     * <p>
111     * Adds a package that will be used by {@link #exec(String)} to load command from.
112     * </p>
113     *
114     * @param pkg
115     *            package where commands are located
116     * @throws IllegalArgumentException
117     *             thrown if the package name is null or empty string
118     */
119    public void addCommandPackage(String pkg) {
120        if ("".equals(pkg) || pkg == null) {
121            throw new IllegalArgumentException("package name must not be null or empty string");
122        }
123        commandPackageList.add(pkg);
124    }
125
126    /**
127     * <p>
128     * Executes the command defined by string. A command has the following form (mix of EBNF and
129     * natural language):
130     * </p>
131     * <code>
132     * &lt;command&gt; := &lt;commandname&gt;&lt;whitespace&gt;{&lt;parameter&gt;}<br>
133     * &lt;commandname&gt; := String without whitespaces. Has to be a valid Java class name<br>
134     * &lt;parameter&gt; := &lt;string&gt;|&lt;stringarray&gt;<br>
135     * &lt;string&gt; := &lt;stringwithoutwhitespaces&gt;|&lt;stringwithwhitespaces&gt;
136     * &lt;stringwithoutwhitespaces&gt; := a string without whitespaces<br>
137     * &lt;stringwithoutwhitespaces&gt; := a string, that can have whitespaces, but must be in double quotes<br>
138     * &lt;stringarray&gt; := "["&lt;string&gt;{&lt;whitespace&gt;&lt;string&gt;"]"
139     * </code>
140     *
141     * @param command
142     *            the command as a string
143     */
144    public void exec(String command) {
145        Console.commandNotification(command);
146        Command cmd = null;
147        CommandParser parser = new CommandParser();
148        parser.parse(command);
149        for (int i = 0; cmd == null && i < commandPackageList.size(); i++) {
150            cmd = loadCMD(commandPackageList.get(i) + "." + cmdPrefix + parser.getCommandName());
151        }
152        if (cmd == null) { // check if command is available as default command
153            cmd = loadCMD(defaultPackage + "." + cmdPrefix + parser.getCommandName());
154        }
155        if (cmd == null) {
156            Console.println("Unknown command");
157        }
158        else {
159            try {
160                cmd.run(parser.getParameters());
161            }
162            catch (IllegalArgumentException e) {
163                Console.println("Usage: " + cmd.help());
164            }
165        }
166    }
167
168    /**
169     * <p>
170     * Helper method that loads a class and tries to cast it to {@link Command}.
171     * </p>
172     *
173     * @param className
174     *            qualified name of the class (including package name)
175     * @return if class is available and implement {@link Command} and instance of the class, null
176     *         otherwise
177     */
178    public Command loadCMD(String className) {
179        Command cmd = null;
180        try {
181            Class<?> cmdClass = Class.forName(className);
182            cmd = (Command) cmdClass.newInstance();
183        }
184        catch (NoClassDefFoundError e) {
185            String[] splitResult = e.getMessage().split("CMD");
186            String correctName = splitResult[splitResult.length - 1].replace(")", "");
187            Console.println("Did you mean " + correctName + "?");
188        }
189        catch (ClassNotFoundException e) {}
190        catch (IllegalAccessException e) {}
191        catch (InstantiationException e) {}
192        catch (ClassCastException e) {
193            Console.traceln(Level.WARNING, className + "found, but does not implement Command");
194        }
195        return cmd;
196    }
197   
198    /**
199     * <p>
200     * Helper method that loads a class and tries to cast it to {@link Command}.
201     * </p>
202     *
203     * @param className
204     *            qualified name of the class (including package name)
205     * @return if class is available and implement {@link Command} and instance of the class, null
206     *         otherwise
207     */
208    public Command getCMD(String commandName) {
209        Command cmd = null;
210        for (int i = 0; cmd == null && i < commandPackageList.size(); i++) {
211            cmd = loadCMD(commandPackageList.get(i) + "." + cmdPrefix + commandName);
212        }
213        if (cmd == null) { // check if command is available as default command
214            cmd = loadCMD(defaultPackage + "." + cmdPrefix + commandName);
215        }
216        return cmd;
217    }
218
219    /**
220     * <p>
221     * reads all available commands from the registered command packages and returns a list of their
222     * names
223     * </p>
224     *
225     * @return an array containing the names of the available commands.
226     */
227    public Command[] getAvailableCommands() {
228        if (availableCommands == null) {
229            List<Command> commands = new ArrayList<Command>();
230            List<String> packages = new ArrayList<String>();
231            packages.addAll(commandPackageList);
232            packages.add(defaultPackage);
233
234            FilenameFilter filter = new FilenameFilter() {
235                @Override
236                public boolean accept(File dir, String name) {
237                    return
238                        (name != null) && (name.startsWith(cmdPrefix)) && (name.endsWith(".class"));
239                }
240            };
241
242            SortedSet<String> classNames = new TreeSet<String>(new Comparator<String>() {
243                @Override
244                public int compare(String arg1, String arg2) {
245                    String str1 = arg1.substring(arg1.lastIndexOf('.') + cmdPrefix.length() + 1);
246                    String str2 = arg2.substring(arg2.lastIndexOf('.') + cmdPrefix.length() + 1);
247                    return str1.compareTo(str2);
248                }
249
250            });
251
252            for (String packageName : packages) {
253                String path = packageName.replace('.', '/');
254                try {
255                    Enumeration<URL> resources = ClassLoader.getSystemResources(path);
256
257                    while (resources.hasMoreElements()) {
258                        URL resource = resources.nextElement();
259                        File packageDir = new File(resource.getFile());
260
261                        if (packageDir.isDirectory()) {
262                            for (File classFile : packageDir.listFiles(filter)) {
263                                String className = classFile.getName().substring
264                                    (0, classFile.getName().lastIndexOf('.'));
265                                classNames.add(packageName + "." + className);
266                            }
267                        }
268                        else {
269                            int index = resource.getFile().lastIndexOf('!');
270                            if ((index > 0) && (resource.getFile().startsWith("file:")) &&
271                                (resource.getFile().endsWith("!/" + path)))
272                            {
273                                String jarFile =
274                                    resource.getFile().substring("file:".length(), index);
275
276                                // we have to read the package content from a jar file
277                                JarInputStream jarInputStream = null;
278                                try {
279                                    jarInputStream =
280                                        new JarInputStream(new FileInputStream(jarFile));
281                                    JarEntry entry = null;
282                                    do {
283                                        entry = jarInputStream.getNextJarEntry();
284                                        if ((entry != null) && (!entry.isDirectory()) &&
285                                                (entry.getName().startsWith(path)))
286                                        {
287                                            String className = entry.getName().substring
288                                                (path.length() + 1, entry.getName().lastIndexOf('.'));
289                                            classNames.add(packageName + "." + className);
290                                        }
291                                    }
292                                    while (entry != null);
293                                }
294                                catch (Exception e) {
295                                    e.printStackTrace();
296                                    Console.traceln(Level.WARNING, "could not read contents of " +
297                                                    "jar " + jarFile);
298                                }
299                                finally {
300                                    if (jarInputStream != null) {
301                                        jarInputStream.close();
302                                    }
303                                }
304
305                            }
306                        }
307                    }
308                }
309                catch (IOException e) {
310                    Console.traceln
311                        (Level.WARNING, "could not read commands of package " + packageName);
312                }
313            }
314
315            for (String className : classNames) {
316                // class may still be inner classes. Therefore load the command, to
317                // see if it is really available and a command.
318                Command cmd = loadCMD(className);
319                if (cmd != null) {
320                    commands.add(cmd);
321                }
322            }
323
324            availableCommands = commands.toArray(new Command[commands.size()]);
325        }
326       
327        return availableCommands;
328    }
329   
330    /**
331     * <p>
332     * Get a copy of the currently registered command packages.
333     * </p>
334     *
335     * @return currently registered command packages
336     */
337    public List<String> getCommandPackages() {
338        List<String> commandPackageListCopy = new ArrayList<String>(commandPackageList);
339        commandPackageListCopy.add(0, defaultPackage);
340        return commandPackageListCopy;
341    }
342
343    /**
344     * <p>
345     * this method method performs an auto completion of the provided String as far as possible
346     * regarding the available commands. It auto completes to the full command name, if only
347     * one command matches the given prefix. It auto completes to the common denominator, if
348     * several commands match the prefix
349     * </p>
350     *
351     * @param commandPrefix the prefix to be auto completed
352     *
353     * @return as described
354     */
355    public String autoCompleteCommand(String commandPrefix) {
356        Command[] commands = getAvailableCommands();
357       
358        List<Command> matchingCommands = new ArrayList<Command>();
359        for (Command command : commands) {
360            String commandName = command.getClass().getSimpleName().substring(3);
361            if (commandName.startsWith(commandPrefix)) {
362                matchingCommands.add(command);
363            }
364        }
365       
366        StringBuffer completedPrefix = new StringBuffer(commandPrefix);
367       
368        boolean foundCompletion = false;
369       
370        while (!foundCompletion) {
371            char nextCompletionChar = 0;
372            for (Command command : matchingCommands) {
373                String commandName = command.getClass().getSimpleName().substring(3);
374                if (commandName.length() > completedPrefix.length()) {
375                    if (nextCompletionChar == 0) {
376                        nextCompletionChar = commandName.charAt(completedPrefix.length());
377                    }
378                    else if (nextCompletionChar != commandName.charAt(completedPrefix.length())) {
379                        foundCompletion = true;
380                    }
381                }
382                else {
383                    foundCompletion = true;
384                }
385            }
386           
387            if (!foundCompletion && (nextCompletionChar != 0)) {
388                completedPrefix.append(nextCompletionChar);
389            }
390            else {
391                foundCompletion = true;
392            }
393        }
394       
395        return completedPrefix.toString();
396    }
397}
Note: See TracBrowser for help on using the repository browser.