package votorola.a; // Copyright 2007-2008, 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. import gnu.getopt.*; import java.util.*; import javax.mail.internet.*; import votorola._.*; import votorola.a.locale.*; import votorola.g.lang.*; import votorola.g.mail.*; import votorola.g.option.*; /** A responder to a voter command. */ public interface CommandResponder { // - C o m m a n d - R e s p o n d e r ------------------------------------------------ /** Returns true if the command may be issued by an anonymous or unauthenticated user; * false if an authenticated {@linkplain CommandResponder.Session#userEmail() * user email address} is required. */ public boolean acceptsAnonymousIssue(); /** Returns the localized name of the command. */ public String commandName( Session session ); /** Replies with instructions on using the command. */ public void help( Session session ); /** Responds to an invocation of the command. * * @param argv array of command name (index 0) * and any arguments (indeces 1..*) * * @throws CommandResponder.AnonymousIssueException if session * is anonymous, but the responder requires a voter email address * * @return any soft exception, of temporary cause (which might clear up * on a retry); or null if none occured */ public Exception respond( String[] argv, Session session ); // ==================================================================================== /** Thrown when a command cannot be accepted * because it was issued anonymously. * * @see CommandResponder#acceptsAnonymousIssue() */ public static final class AnonymousIssueException extends VotorolaRuntimeException { public AnonymousIssueException( String commandName ) { super( "'" + commandName + "' cannot be issued anonymously, it requires a voter email address" ); } } // ==================================================================================== /** Base implementation of a command responder. */ public static abstract class Base implements CommandResponder { /** Constructs a Base. */ public Base( ElectoralService electoralService, String keyPrefix ) { this.electoralService = electoralService; this.keyPrefix = keyPrefix; } // -------------------------------------------------------------------------------- /** Compiles a minimal map of options for a responder. */ protected static HashMap compileBaseOptions( final Session session ) { final HashMap optionMap = new HashMap(); final ResourceBundle bundle = session.replyBuilder().bundle(); String key; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - key = "a.CommandResponder.option.help"; optionMap.put( key, new Option( bundle.getString(key), Option.NO_ARGUMENT )); // - - - return optionMap; } /** Returns the canonical form of an email address argument. * * @param arg argument specifying the email address, as input by the user * @param commandName localized name of this command responder * * @return canonical form of email address; or null, * if the argument is null * * @throws AddressException if the argument specifies a malformed address; * in which case a suitable error message will already be output * to the session reply builder */ public static String canonicalEmail( final String arg, final String commandName, final Session session ) throws AddressException { try { return arg == null? null: InternetAddressX.canonicalAddress( arg ); } catch( AddressException x ) { session.replyBuilder().lappendln( "a.CommandResponder.malformedEmail(1,2,3)", commandName, arg, x.getMessage() ); throw x; } } protected final ElectoralService electoralService; protected final String keyPrefix; /** Parses the arguments against a formal option map, * {@linkplain Option#addOccurence registering actual occurences} in it. * * @param argv array of command name and arguments, * per {@linkplain CommandResponder#respond(String[],CommandResponder.Session) respond}(argv,session); * it will be re-arranged so that all options come first * @param optionMap against which to interpret argv * * @return Normally returns the index of first non-option argument, * as returned by Getopt.getOptind() after parsing, * but incremented by one to skip past the command itself. * Returns -1 if a parsing error occurs, * in which case an error message and help prompt * have already been appended to the reply. */ protected int parse( final String[] argv, final Map optionMap, final Session session ) { // cf. votorola.g.option.GetoptX.parse final String commandName = argv[0]; final ReplyBuilder replyB = session.replyBuilder(); final Option[] optionArray = optionMap.values().toArray( new Option[optionMap.size()] ); final Getopt getopt = new Getopt( commandName, argv, ":", optionArray ); // rearranges argv getopt.setOpterr( false ); // do our own error handling parse: for( ;; ) { final int o = getopt.getopt(); switch( o ) { case -1: break parse; case 0: optionArray[getopt.getLongind()].addOccurence( getopt.getOptarg() ); break; case ':': replyB.lappend( "a.CommandResponder.missingValueForOption(1)", commandName ); // Expected 'option=value'. Unfortunately, there is no easy way to get the name of the valueless option from getopt. replyB.append( '\n' ); replyB.lappendlnn( "a.CommandResponder.helpPrompt(1)", commandName ); return -1; case '?': replyB.lappend( "a.CommandResponder.unrecognizedOption(1)", commandName ); // no easy way to get the name of the unrecognized option from getopt replyB.append( '\n' ); replyB.lappendlnn( "a.CommandResponder.helpPrompt(1)", commandName ); return -1; default: assert false; } } assert argv[getopt.getOptind()] == commandName; // after rearranging return getopt.getOptind() + 1; // skip the command name } // - C o m m a n d - R e s p o n d e r -------------------------------------------- /** Returns false. */ public boolean acceptsAnonymousIssue() { return false; } /** Returns the localized string for keyPrefix + "commandName". * * @see * locale/CR.properties */ public String commandName( Session session ) { return session.replyBuilder().bundle().getString( keyPrefix + "commandName" ); } /** Calls X.{@linkplain CommandResponder.X#helpDefault(String,CommandResponder.Session) helpDefault}(keyPrefix,session). */ public void help( final Session session ) { X.helpDefault( keyPrefix, session ); } } // ==================================================================================== /** A user session in a command response inteface. It is intended for short service, * to respond to a single email message, for example, or a single HTTP request. * Its facilities include a keyed Map of ad hoc, session-scope variables. */ public static @ThreadRestricted final class Session extends HashMap implements ElectoralSubserver.UserSession { /** Constructs a Session. * * @param userEmail per {@linkplain #userEmail() userEmail}() * @param userTrustLevel per {@linkplain #userTrustLevel() userTrustLevel}() * @param bunA per {@linkplain #bunA() bunA}() * @param replyBuilder per {@linkplain #replyBuilder() replyBuilder}() */ public Session( String userEmail, int userTrustLevel, BundleFormatter bunA, ReplyBuilder replyBuilder ) { this.bunA = bunA; this.replyBuilder = replyBuilder; this.userEmail = userEmail; } // -------------------------------------------------------------------------------- /** The application (A) bundle formatter for this session. * It uses bundle base name 'votorola.a.locale.A'. * * @see * locale/A.properties * @see #replyBuilder() */ public BundleFormatter bunA() { return bunA; } private final BundleFormatter bunA; /** The command-response (CR) builder to use in replying to commands. * It uses bundle base name 'votorola.a.locale.CR'. * * @see * locale/CR.properties * @see #bunA() */ public ReplyBuilder replyBuilder() { return replyBuilder; } private final ReplyBuilder replyBuilder; // - E l e c t o r a l - S u b s e r v e r . U s e r - S e s s i o n -------------- public String userEmail() { return userEmail; } private final String userEmail; public int userTrustLevel() { return userTrustLevel; } private int userTrustLevel; // ================================================================================ /** A comparator to compare command-responders * by {@linkplain #commandName(CommandResponder.Session) commandName}. */ public final class ResponderNameComparator implements Comparator { // - C o m p a r a t o r ------------------------------------------------------ public int compare( CommandResponder r1, CommandResponder r2 ) { return r1.commandName(Session.this).compareTo( r2.commandName(Session.this) ); } // - O b j e c t -------------------------------------------------------------- // /** Returns true if o is a NameComparator with an 'equals' // * resource bundle in its session. // */ // public @Override final boolean equals( Object o ) ////// FIX session ref. syntax if ever needed // { // if( o == null || !o.getClass().equals(NameComparator.class) ) return false; // // final NameComparator c = (NameComparator)o; // return session.replyBuilder().bundle().equals( c.session.replyBuilder().bundle() ); // } } } // ==================================================================================== /** Command responder utilities. */ public static final class X { private X() {} /** Replies with the localized strings for keyPrefix + "help.summary", * "help.syntax", and "help.body". * * @see * locale/CR.properties */ public static void helpDefault( final String keyPrefix, final Session session ) { final ReplyBuilder replyB = session.replyBuilder(); replyB.lappendlnn( keyPrefix + "help.summary" ); replyB.indent( 4 ).setWrapping( false ); replyB.lappendlnn( keyPrefix + "help.syntax" ); replyB.exdent( 4 ).setWrapping( true ); replyB.lappendlnn( keyPrefix + "help.body" ); } } }