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}