001package votorola.a.response; // Copyright 2007-2008, 2012-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. 002 003import gnu.getopt.*; 004import java.util.*; 005import javax.mail.internet.*; 006import votorola.a.*; 007import votorola.a.voter.*; 008import votorola.g.*; 009import votorola.g.locale.*; 010import votorola.g.lang.*; 011import votorola.g.mail.*; 012import votorola.g.option.*; 013 014 015/** A responder to a voter command. 016 */ 017public interface CommandResponder 018{ 019 020 // - C o m m a n d - R e s p o n d e r ------------------------------------------------ 021 022 023 /** Answers whether the command may be issued by unauthenticated users. 024 */ 025 public boolean acceptsAnonymousIssue(); 026 027 028 029 /** Returns the localized name of the command. 030 */ 031 public String commandName( Session session ); 032 033 034 035 /** Replies with instructions on using the command. 036 */ 037 public void help( Session session ); 038 039 040 041 /** Responds to an invocation of the command. 042 * 043 * @param argv an array comprising the command name (index 0) and any arguments 044 * (indeces 1..*). 045 * @return any soft exception of a potentially temporary cause that might clear 046 * up on retry, or null if none occured. 047 * 048 * @throws CommandResponder.AnonymousIssueException if session is anonymous, but 049 * the responder requires a voter email address. 050 */ 051 public Exception respond( String[] argv, Session session ); 052 053 054 055 // ==================================================================================== 056 057 058 /** Thrown when a command cannot be accepted because it was issued anonymously. 059 * 060 * @see CommandResponder#acceptsAnonymousIssue() 061 */ 062 public static final class AnonymousIssueException extends VotorolaRuntimeException 063 { 064 065 public AnonymousIssueException( String commandName ) 066 { 067 super( "'" + commandName + 068 "' cannot be issued anonymously, it requires a voter email address" ); 069 } 070 071 072 } 073 074 075 076 // ==================================================================================== 077 078 079 /** Base implementation of a command responder. 080 */ 081 public static abstract class Base implements CommandResponder 082 { 083 084 /** Constructs a Base. 085 */ 086 public Base( VoterService voterService, String keyPrefix ) 087 { 088 this.voterService = voterService; 089 this.keyPrefix = keyPrefix; 090 } 091 092 093 // -------------------------------------------------------------------------------- 094 095 096 /** Compiles a minimal map of options for a responder. 097 */ 098 protected static HashMap<String,Option> compileBaseOptions( final Session session ) 099 { 100 final HashMap<String,Option> optionMap = new HashMap<String,Option>(); 101 final ResourceBundle bundle = session.replyBuilder().bundle(); 102 String key; 103 104 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 105 key = "a.voter.CommandResponder.option.help"; 106 optionMap.put( key, new Option( bundle.getString(key), Option.NO_ARGUMENT )); 107 108 // - - - 109 return optionMap; 110 } 111 112 113 /** Translates a personal identifier to a canonical email address. 114 * 115 * @param idString the personal identifier as input by the user in the form 116 * of either an email address or a mailish username. 117 * @param commandName the localized name of this command responder. 118 * 119 * @return canonical form of email address, or null if 'id' is null. 120 * 121 * @see <a href='http://reluk.ca/project/_/mailish/MailishUsername.xht' 122 * >MailishUsername</a> 123 * @throws AddressException if 'id' cannot be translated, in which case an 124 * error message is already output to the session reply builder. 125 */ 126 public static String canonicalEmail( final String idString, final String commandName, 127 final Session session ) throws AddressException 128 { 129 if( idString == null ) return null; 130 131 final String email; 132 try 133 { 134 if( idString.indexOf('@') > 0 ) 135 { 136 email = InternetAddressX.canonicalAddress( idString ); 137 } 138 else email = IDPair.toInternetAddress( idString ).getAddress(); 139 } 140 catch( final AddressException x ) 141 { 142 session.replyBuilder().lappendln( "a.voter.CommandResponder.canonicalEmail(1,2,3)", 143 commandName, idString, x.getMessage() ); 144 throw x; 145 } 146 return email; 147 } 148 149 150 protected final VoterService voterService; 151 152 153 protected final String keyPrefix; 154 155 156 /** Parses the arguments against a formal option map, {@linkplain 157 * Option#addOccurence registering actual occurences} in it. 158 * 159 * @param argv the array of command name and arguments, per {@linkplain 160 * CommandResponder#respond(String[],CommandResponder.Session) 161 * respond}(argv,session). It will be re-arranged so that all options come 162 * first. 163 * @param optionMap the map against which to interpret argv. 164 * 165 * @return Normally returns the index of first non-option argument, as 166 * returned by Getopt.getOptind() after parsing, but incremented by one to 167 * skip past the command itself. Returns -1 if a parsing error occurs, in 168 * which case an error message and help prompt have already been appended 169 * to the reply. 170 */ 171 protected int parse( final String[] argv, final Map<String,Option> optionMap, 172 final Session session ) 173 { 174 // cf. votorola.g.option.GetoptX.parse 175 176 final String commandName = argv[0]; 177 178 final ReplyBuilder replyB = session.replyBuilder(); 179 final Option[] optionArray = optionMap.values().toArray( new Option[optionMap.size()] ); 180 final Getopt getopt = new Getopt( commandName, argv, ":", optionArray ); // rearranges argv 181 getopt.setOpterr( false ); // do our own error handling 182 parse: for( ;; ) 183 { 184 final int o = getopt.getopt(); 185 switch( o ) 186 { 187 case -1: 188 break parse; 189 190 case 0: 191 optionArray[getopt.getLongind()].addOccurence( getopt.getOptarg() ); 192 break; 193 194 case ':': 195 replyB.lappend( "a.voter.CommandResponder.missingValueForOption(1)", commandName ); // Expected 'option=value'. Unfortunately, there is no easy way to get the name of the valueless option from getopt. 196 replyB.append( '\n' ); 197 replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); 198 return -1; 199 200 case '?': 201 replyB.lappend( "a.voter.CommandResponder.unrecognizedOption(1)", commandName ); // no easy way to get the name of the unrecognized option from getopt 202 replyB.append( '\n' ); 203 replyB.lappendlnn( "a.voter.CommandResponder.helpPrompt(1)", commandName ); 204 return -1; 205 206 default: 207 assert false; 208 } 209 } 210 assert argv[getopt.getOptind()] == commandName; // after rearranging 211 return getopt.getOptind() + 1; // skip the command name 212 } 213 214 215 // - C o m m a n d - R e s p o n d e r -------------------------------------------- 216 217 218 /** Returns false. 219 */ 220 public boolean acceptsAnonymousIssue() { return false; } 221 222 223 /** Returns the localized string for keyPrefix + "commandName". 224 * 225 * @see <a href='../../../../../a/locale/CR.properties'> 226 * ../locale/CR.properties</a> 227 */ 228 public String commandName( Session session ) 229 { 230 return session.replyBuilder().bundle().getString( keyPrefix + "commandName" ); 231 } 232 233 234 /** Calls U.{@linkplain 235 * CommandResponder.U#helpDefault(String,CommandResponder.Session) 236 * helpDefault}(keyPrefix,session). 237 */ 238 public void help( final Session session ) { U.helpDefault( keyPrefix, session ); } 239 240 241 } 242 243 244 245 // ==================================================================================== 246 247 248 /** A service session with a user. It is intended for short service in response to a 249 * single email message, for example, or a single HTTP request. It embodies a keyed 250 * map of ad hoc, session-scope variables. 251 */ 252 public static @ThreadRestricted final class Session extends HashMap<Object,Object> 253 implements AuthenticatedUser, ServiceSession 254 { 255 256 257 /** Constructs a Session. 258 * 259 * @see #voterInterface() 260 * @see #email() 261 * @see #trustLevel() 262 * @see #bunA() 263 * @see #replyBuilder() 264 */ 265 public Session( VoterInterface _voterInterface, String _email, int _trustLevel, 266 BundleFormatter _bunA, ReplyBuilder _replyBuilder ) 267 { 268 if( _email == null ) throw new NullPointerException(); // fail fast 269 270 voterInterface = _voterInterface; 271 email = _email; 272 trustLevel = _trustLevel; 273 bunA = _bunA; 274 replyBuilder = _replyBuilder; 275 } 276 277 278 279 // -------------------------------------------------------------------------------- 280 281 282 /** The application (A) bundle formatter for this session. It uses bundle base 283 * name 'votorola.a.locale.A'. 284 * 285 * @see <a href='../../../../../a/locale/A.properties'> 286 * ../locale/A.properties</a> 287 * @see #bunCR() 288 */ 289 public BundleFormatter bunA() { return bunA; } 290 291 292 private final BundleFormatter bunA; 293 294 295 296 /** The command/response (CR) bundle formatter for this session. It uses bundle base 297 * name 'votorola.a.locale.CR'. 298 * 299 * @return the reply builder. 300 * @see <a href='../../../../../a/locale/CR.properties'> 301 * ../locale/CR.properties</a> 302 * @see #bunA() 303 * @see #replyBuilder() 304 */ 305 public BundleFormatter bunCR() { return replyBuilder; } 306 307 308 309 /** The command-response (CR) builder to use in replying to commands. It uses 310 * bundle base name 'votorola.a.locale.CR'. 311 * 312 * @see <a href='../../../../../a/locale/CR.properties'> 313 * ../locale/CR.properties</a> 314 * @see #bunA() 315 * @see #bunCR() 316 */ 317 public ReplyBuilder replyBuilder() { return replyBuilder; } 318 319 320 private final ReplyBuilder replyBuilder; 321 322 323 324 /** The voter interface that is providing this session. 325 */ 326 public VoterInterface voterInterface() { return voterInterface; } 327 328 329 private final VoterInterface voterInterface; 330 331 332 333 // - A u t h e n t i c a t e d - U s e r ------------------------------------------ 334 335 336 public String email() { return email; } 337 338 339 private final String email; 340 341 342 343 public int trustLevel() { return trustLevel; } 344 345 346 private final int trustLevel; 347 348 349 350 // - S e r v i c e - S e s s i o n ------------------------------------------------ 351 352 353 public AuthenticatedUser user() { return Session.this; } 354 355 356 357 public AuthenticatedUser userOrNobody() { return Session.this; } 358 359 360 361 // ================================================================================ 362 363 364 /** A comparator to compare command-responders by {@linkplain 365 * #commandName(CommandResponder.Session) commandName}. 366 */ 367 public final class ResponderNameComparator implements Comparator<CommandResponder> 368 { 369 370 // - C o m p a r a t o r ------------------------------------------------------ 371 372 373 public int compare( CommandResponder r1, CommandResponder r2 ) 374 { 375 return r1.commandName(Session.this).compareTo( r2.commandName(Session.this) ); 376 } 377 378 379 // - O b j e c t -------------------------------------------------------------- 380 381 382 // /** Returns true if o is a NameComparator with an 'equals' 383 // * resource bundle in its session. 384 // */ 385 // public @Override final boolean equals( Object o ) ////// FIX session ref. syntax if ever needed 386 // { 387 // if( o == null || !o.getClass().equals(NameComparator.class) ) return false; 388 // 389 // final NameComparator c = (NameComparator)o; 390 // return session.replyBuilder().bundle().equals( c.session.replyBuilder().bundle() ); 391 // } 392 393 } 394 395 } 396 397 398 399 // ==================================================================================== 400 401 402 /** Command responder utilities. 403 */ 404 public static final class U 405 { 406 407 private U() {} 408 409 410 /** Replies with the localized strings for keyPrefix + "help.summary", 411 * "help.syntax", and "help.body". 412 * 413 * @see <a href='../../../../../a/locale/CR.properties'> 414 * ../locale/CR.properties</a> 415 */ 416 public static void helpDefault( final String keyPrefix, final Session session ) 417 { 418 final ReplyBuilder replyB = session.replyBuilder(); 419 replyB.lappendlnn( keyPrefix + "help.summary" ); 420 replyB.indent( 4 ).setWrapping( false ); 421 replyB.lappendlnn( keyPrefix + "help.syntax" ); 422 replyB.exdent( 4 ).setWrapping( true ); 423 replyB.lappendlnn( keyPrefix + "help.body" ); 424 } 425 426 427 } 428 429 430}