001/* ---------------------------------------------------------------------------- 002 The Kiwi Toolkit - A Java Class Library 003 Copyright (C) 1998-2004 Mark A. Lindner 004 005 This library is free software; you can redistribute it and/or 006 modify it under the terms of the GNU General Public License as 007 published by the Free Software Foundation; either version 2 of the 008 License, or (at your option) any later version. 009 010 This library is distributed in the hope that it will be useful, 011 but WITHOUT ANY WARRANTY; without even the implied warranty of 012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 General Public License for more details. 014 015 You should have received a copy of the GNU General Public License 016 along with this library; if not, write to the Free Software 017 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 018 02111-1307, USA. 019 020 The author may be contacted at: mark_a_lindner@yahoo.com 021 ---------------------------------------------------------------------------- 022 $Log: CommandDispatcher.java,v $ 023 Revision 1.5 2004/05/05 21:22:45 markl 024 Comment header updates. 025 026 Revision 1.4 2003/01/19 09:42:38 markl 027 Javadoc & comment header updates. 028 029 Revision 1.3 2001/03/12 02:57:38 markl 030 Source code cleanup. 031 032 Revision 1.2 1999/01/10 03:41:46 markl 033 added GPL header & RCS tag 034 ---------------------------------------------------------------------------- 035*/ 036 037package kiwi.util; 038 039import java.util.*; 040import java.lang.reflect.*; 041 042/** A class that makes use of the reflection API to implement a generic, 043 * reusable, and flexible command processor. 044 * <p> 045 * The <code>CommandDispatcher</code> analyzes a helper class (which 046 * implements the <code>CommandProcessor</code> interface), searching for 047 * methods whose names begin with the prefix <code>cmd_</code>. This 048 * information is used to build a command dictionary that maps command names 049 * to their associated processing methods. 050 * <p> 051 * The general idea is that the <code>CommandDispatcher</code> will be fed 052 * command strings that consist of a command name followed by a whitespace- 053 * separated list of arguments. This string is tokenized into words. The first 054 * word is the command name and is used to look up an associated method in the 055 * command dictionary. For example, if the command is <i>hello</i>, the 056 * associated method in the helper class would be <code>cmd_hello()</code>. If 057 * a matching method is not found, the helper object is messaged with an 058 * <code>unknownCommandError()</code>. 059 * <p> 060 * If the command is found, then the remaining tokens are counted to determine 061 * the number of arguments that the command is being invoked with. Some 062 * commands may have more than one form, or may have optional arguments. In 063 * this case, the associated command processing method in the helper class may 064 * be overloaded to handle all of the separate (legal) forms. The argument 065 * count is used to select an appropriate command handler. Note that no two 066 * command processing methods of the same name may have the same number of 067 * arguments, as this would produce a parsing ambiguity. Thus it is not valid 068 * to have <code>void cmd_hello(String s, int i)</code> and 069 * <code>void cmd_hello(int a, int b)</code>. If an appriate method cannot be 070 * found, the helper object is messaged with an 071 * <code>argumentCountError()</code>. 072 * <p> 073 * The arguments to the command (which are string tokens) are cast to the 074 * appropriate data types for the method. Thus if the command processor method 075 * for <i>hello</i> is declared as <code>void cmd_hello(String s, int i) 076 * </code>, then the first argument is left as a string and the second 077 * argument is converted to an <code>Integer</code> object. The following 078 * object types are supported for arguments: <code>String</code>, 079 * <code>int</code>, <code>Integer</code>, <code>boolean</code>, 080 * <code>Boolean</code>, <code>short</code>, <code>Short</code>, 081 * <code>long</code>, <code>Long</code>, <code>float</code>, 082 * <code>Float</code>, <code>double</code>, <code>Double</code>. If an error 083 * occurrs during argument conversion, the helper object is messaged with an 084 * <code>arugmentFormatError()</code>. 085 * <p> 086 * Once argument conversion is complete, the command processor method in the 087 * helper object is invoked with the argument list. If an error occurrs during 088 * the invocation, the helper object is messaged with an 089 * <code>invocationError()</code>. 090 * 091 * @author Mark Lindner 092 */ 093 094public class CommandDispatcher 095 { 096 private CommandProcessor processor; 097 private Hashtable cmds; 098 private static final Class argtypes[] 099 = { Integer.class, Float.class, Double.class, Short.class, Long.class, 100 java.lang.String.class, Boolean.class }; 101 private static final Class argtypes2[] 102 = { int.class, float.class, double.class, short.class, Long.class, 103 java.lang.String.class, boolean.class }; 104 private static final Class stringConstructor[] = { java.lang.String.class }; 105 106 /** Construct a new <code>CommandDispatcher</code> for the given helper 107 * class. 108 * 109 * @param processor The helper class that serves as the command processor 110 * and error handler. 111 */ 112 113 public CommandDispatcher(CommandProcessor processor) 114 { 115 this.processor = processor; 116 cmds = new Hashtable(); 117 118 // analyze the processor class and build a command dictionary 119 120 Method methods[] = processor.getClass().getMethods(); 121 122 NEXTCMD: 123 for(int i = 0; i < methods.length; i++) 124 { 125 String nm = methods[i].getName(); 126 127 // ignore methods whose names don't begin with "cmd_" 128 129 if(!(nm.startsWith("cmd_"))) continue; 130 131 // check all the method's parameter types to ensure that they are of 132 // supported types; ignore methods that don't pass this test 133 134 Class args[] = methods[i].getParameterTypes(); 135 for(int j = 0; j < args.length; j++) 136 if(!isSupported(args[j])) continue NEXTCMD; 137 138 // strip off the "cmd_" prefix and try to look up the vector for this 139 // method name in the hash table 140 141 String c = nm.substring(4); 142 Vector v = (Vector)cmds.get(c); 143 144 // if this is the first method of this name that we've encountered, 145 // create the vector in the hash table 146 147 if(v == null) 148 { 149 v = new Vector(); 150 cmds.put(c, v); 151 } 152 153 // add the method object to the vector 154 155 v.addElement(methods[i]); 156 } 157 } 158 159 /** Dispatch a command string to the appropriate processing method. 160 * 161 * @param s The command string to be parsed and passed to its associated 162 * command processing method. 163 */ 164 165 public void dispatch(String s) 166 { 167 StringTokenizer st = new StringTokenizer(s); 168 169 // if there are no tokens on the line, ignore the line 170 171 if(!st.hasMoreTokens()) return; 172 173 // pull the first token off the string; this is the command line 174 // try to find the vector for this command in the hashtable 175 176 String cmd = st.nextToken(); 177 Vector v = (Vector)cmds.get(cmd); 178 if(v == null) 179 { 180 // if we can't find one, message an error & abort 181 182 processor.unknownCommandError(cmd); 183 return; 184 } 185 186 // enumerate through the vector, examining each overloaded method in turn 187 188 Enumeration e = v.elements(); 189 Method m = null; 190 Class params[] = null; 191 boolean found = false; 192 Constructor cons = null; 193 194 while(e.hasMoreElements()) 195 { 196 m = (Method)e.nextElement(); 197 params = m.getParameterTypes(); 198 199 // if the parameter count matches the number of tokens remaining 200 // (arguments) then we've found our method 201 202 if(params.length == st.countTokens()) 203 { 204 found = true; 205 break; 206 } 207 } 208 209 if(!found) 210 { 211 // if we didn't find a method, message an error & abort 212 213 processor.argumentCountError(cmd); 214 return; 215 } 216 217 // construct the argument list to pass to the method 218 219 Object args[] = new Object[params.length]; 220 221 for(int i = 0; i < params.length; i++) 222 { 223 // get a wrapper class for the nth parameter, and try to locate a 224 // constructor for the wrapper class that takes only a single string as 225 // an argument 226 227 Class clazz = wrapperClass(params[i]); 228 String arg = st.nextToken(); 229 230 try 231 { 232 cons = clazz.getConstructor(stringConstructor); 233 } 234 catch(NoSuchMethodException ex) 235 { 236 // this shouldn't happen, but if it does, we should abort 237 238 processor.argumentFormatError(cmd, arg); 239 return; 240 } 241 242 // instantiate the class with the next token as the argument 243 244 try 245 { 246 args[i] = cons.newInstance(new Object[] { arg }); 247 } 248 catch(Exception ex) 249 { 250 // if we get an instantiation error, then the format of the argument is 251 // incorrect 252 253 processor.argumentFormatError(cmd, arg); 254 return; 255 } 256 } 257 258 // now invoke the method with the argument list, messaging an error and 259 // aborting if we get an exception 260 261 try 262 { 263 m.invoke(processor, args); 264 } 265 catch(Exception ex) 266 { 267 processor.invocationError(cmd, ex); 268 return; 269 } 270 } 271 272 /* Look up the wrapper class for a primitive type class; if we can't find a 273 * wrapper, just return the class that was passed in. 274 */ 275 276 private Class wrapperClass(Class clazz) 277 { 278 for(int i = 0; i < argtypes.length; i++) 279 if(clazz.equals(argtypes2[i])) 280 return(argtypes[i]); 281 282 return(clazz); 283 } 284 285 /* Determine if the specified class is a supported argument type. 286 */ 287 288 private boolean isSupported(Class type) 289 { 290 for(int i = 0; i < argtypes.length; i++) 291 if(type.equals(argtypes[i]) || type.equals(argtypes2[i])) 292 return(true); 293 294 return(false); 295 } 296 297 } 298 299/* end of source file */