package 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. import gnu.getopt.*; import java.util.*; import javax.mail.internet.*; import votorola.a.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.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 ------------------------------------------------ /** Answers whether the command may be issued by unauthenticated users. */ 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 an array comprising the command name (index 0) and any arguments * (indeces 1..*). * @return any soft exception of a potentially temporary cause that might clear * up on retry, or null if none occured. * * @throws CommandResponder.AnonymousIssueException if session is anonymous, but * the responder requires a voter email address. */ 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( VoterService voterService, String keyPrefix ) { this.voterService = voterService; 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.voter.CommandResponder.option.help"; optionMap.put( key, new Option( bundle.getString(key), Option.NO_ARGUMENT )); // - - - return optionMap; } /** Translates a personal identifier to a canonical email address. * * @param idString the personal identifier as input by the user in the form * of either an email address or a mailish username. * @param commandName the localized name of this command responder. * * @return canonical form of email address, or null if 'id' is null. * * @see MailishUsername * @throws AddressException if 'id' cannot be translated, in which case an * error message is already output to the session reply builder. */ public static String canonicalEmail( final String idString, final String commandName, final Session session ) throws AddressException { if( idString == null ) return null; final String email; try { if( idString.indexOf('@') > 0 ) { email = InternetAddressX.canonicalAddress( idString ); } else email = IDPair.toInternetAddress( idString ).getAddress(); } catch( final AddressException x ) { session.replyBuilder().lappendln( "a.voter.CommandResponder.canonicalEmail(1,2,3)", commandName, idString, x.getMessage() ); throw x; } return email; } protected final VoterService voterService; protected final String keyPrefix; /** Parses the arguments against a formal option map, {@linkplain * Option#addOccurence registering actual occurences} in it. * * @param argv the 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 the map 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.voter.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.voter.CommandResponder.helpPrompt(1)", commandName ); return -1; case '?': replyB.lappend( "a.voter.CommandResponder.unrecognizedOption(1)", commandName ); // no easy way to get the name of the unrecognized option from getopt replyB.append( '\n' ); replyB.lappendlnn( "a.voter.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 U.{@linkplain * CommandResponder.U#helpDefault(String,CommandResponder.Session) * helpDefault}(keyPrefix,session). */ public void help( final Session session ) { U.helpDefault( keyPrefix, session ); } } // ==================================================================================== /** A service session with a user. It is intended for short service in response to a * single email message, for example, or a single HTTP request. It embodies a keyed * map of ad hoc, session-scope variables. */ public static @ThreadRestricted final class Session extends HashMap implements AuthenticatedUser, ServiceSession { /** Constructs a Session. * * @see #voterInterface() * @see #email() * @see #trustLevel() * @see #bunA() * @see #replyBuilder() */ public Session( VoterInterface _voterInterface, String _email, int _trustLevel, BundleFormatter _bunA, ReplyBuilder _replyBuilder ) { if( _email == null ) throw new NullPointerException(); // fail fast voterInterface = _voterInterface; email = _email; trustLevel = _trustLevel; bunA = _bunA; replyBuilder = _replyBuilder; } // -------------------------------------------------------------------------------- /** The application (A) bundle formatter for this session. It uses bundle base * name 'votorola.a.locale.A'. * * @see * ../locale/A.properties * @see #bunCR() */ public BundleFormatter bunA() { return bunA; } private final BundleFormatter bunA; /** The command/response (CR) bundle formatter for this session. It uses bundle base * name 'votorola.a.locale.CR'. * * @return the reply builder. * @see * ../locale/CR.properties * @see #bunA() * @see #replyBuilder() */ public BundleFormatter bunCR() { return replyBuilder; } /** 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() * @see #bunCR() */ public ReplyBuilder replyBuilder() { return replyBuilder; } private final ReplyBuilder replyBuilder; /** The voter interface that is providing this session. */ public VoterInterface voterInterface() { return voterInterface; } private final VoterInterface voterInterface; // - A u t h e n t i c a t e d - U s e r ------------------------------------------ public String email() { return email; } private final String email; public int trustLevel() { return trustLevel; } private final int trustLevel; // - S e r v i c e - S e s s i o n ------------------------------------------------ public AuthenticatedUser user() { return Session.this; } public AuthenticatedUser userOrNobody() { return Session.this; } // ================================================================================ /** 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 U { private U() {} /** 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" ); } } }