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

Last change on this file since 2260 was 2260, checked in by pharms, 5 years ago

Update to java 11

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