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

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