001package votorola.a; // Copyright 2007-2010, 2012, 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 java.io.*;
004import java.net.*;
005import java.util.*;
006import java.util.concurrent.locks.*;
007import java.util.logging.*;
008import java.util.regex.*;
009import votorola.a.count.*;
010import votorola.a.response.*;
011import votorola.g.*;
012import votorola.g.lang.*;
013import votorola.g.logging.*;
014import votorola.g.option.*;
015import votorola.g.script.*;
016
017
018/** A facility for voters to access and maintain a category of data on a vote-server.
019  */
020public @ThreadRestricted("holds lock()") abstract class VoterService
021{
022
023
024    /** Partially creates a VoterService.  To complete it, call {@linkplain
025      * #init(ArrayList) init}(responderList).
026      */
027    protected VoterService( VoteServer.Run _vsRun, final ConstructionContext cc )
028    {
029        vsRun = _vsRun;
030        constructionContext = cc;
031
032        name = cc.name;
033
034        if( vsRun.isSingleThreaded() ) lock = vsRun.singleServiceLock();
035        else lock = new ReentrantLock();
036    }
037
038
039
040    /** @param responderList listing only the service-specific responders;
041      *   the general responders will be added by this method.
042      */
043    protected @ThreadRestricted("constructor") final void init(
044      final ArrayList<CommandResponder> responderList )
045    {
046        responderList.add( new CR_Hello( VoterService.this ) );
047        responderList.add( new CR_Help( VoterService.this ));
048        responderList.add( new CR_Version( VoterService.this ));
049     // responderList.trimToSize();
050     // responders = Collections.unmodifiableList( responderList );
051        responderArray = new CommandResponder[responderList.size()];
052        responderList.toArray( responderArray );
053
054        final int mapCapacity = (int)((responderList.size() + 1) / 0.75f) + 1;
055        respondersByClassName = new HashMap<String,CommandResponder>( mapCapacity );
056        CommandResponder duplicate;
057        for( CommandResponder responder: responderList )
058        {
059            logger.finer( "adding responder for '" + name + "': " + responder.getClass().getName() );
060            duplicate = respondersByClassName.put( responder.getClass().getName(), responder );
061            assert duplicate == null : "single responder of class:" + responder.getClass();
062        }
063    }
064
065
066
067   // - V o t e r - S e r v i c e --------------------------------------------------------
068
069
070    /** Looks up the responder of the specified command, and sends the command
071      * to it.  Or, if the look-up fails, replies that the command is unrecognized.
072      *
073      *     @param argArray an array containing the command name and arguments,
074      *         per CommandResponder.respond(argv,session).
075      *     @return any soft exception, per CommandResponder.respond(argv,session);
076      *         or null if none occured.
077      *
078      *     @see CommandResponder#respond(String[],CommandResponder.Session)
079      */
080    public Exception dispatch( final String[] argArray,
081      final CommandResponder.Session commandSession )
082    {
083        return dispatch( argArray, commandSession, responderForCommand( argArray, commandSession ));
084    }
085
086
087
088    /** Sends a command to its responder, if one is specified.  Or, if none is specified,
089      * replies that the command is unrecognized.
090      *
091      *     @param argArray an array containing the command name and arguments,
092      *         per CommandResponder.respond(argv,session).
093      *     @param responder the responder for the command, or null if there is none.
094      *
095      *     @return any soft exception, per CommandResponder.respond(argv,session); or
096      *         null if none occured.
097      *
098      *     @see #responderForCommand(String[],CommandResponder.Session)
099      *     @see CommandResponder#respond(String[],CommandResponder.Session)
100      */
101    public final Exception dispatch( final String[] argArray,
102      final CommandResponder.Session commandSession, final CommandResponder responder )
103    {
104        assert lock.isHeldByCurrentThread();
105        if( responder != null ) return responder.respond( argArray, commandSession );
106
107        final ReplyBuilder replyB = commandSession.replyBuilder();
108        final String commandName = argArray[0];
109        replyB.lappend( "a.VoterService.unrecognized(1)", commandName );
110        final String unrecognizedHelpKey = "a.VoterService.unrecognizedHelp";
111        if( !commandSession.containsKey( unrecognizedHelpKey )) // not yet prompted
112        {
113            commandSession.put( unrecognizedHelpKey, Boolean.TRUE );
114            final CommandResponder help = responderByClassName( CR_Help.class.getName() );
115            if( help != null )
116            {
117                replyB.append( "  " );
118                replyB.lappendlnn( unrecognizedHelpKey );
119                replyB.indent( 4 );
120                replyB.append( help.commandName( commandSession ));
121                replyB.exdent( 4 );
122            }
123        }
124        replyB.appendlnn();
125        return null;
126    }
127
128
129
130    /** Responds to a help command on behalf of the nominal responder, per {@linkplain
131      * CR_Help#respond(String[],Session) respond}(argv,session).
132      */
133    public Exception help( final String[] argv, final CommandResponder.Session session )
134    {
135        helpA( session );
136        helpB( session );
137        helpC( session );
138        return null;
139    }
140
141
142
143    /** Answers whether the named service is (or would be) a non-poll service.  Currently
144      * the names of non-poll services always begin with a lowercase letter, whereas the
145      * names of polls never do.  This is not guaranteed to hold in future, but you are
146      * safe so long as this is your test method.
147      */
148    public static boolean isNonPoll( String name )
149    {
150        return Character.isLowerCase( name.codePointAt( 0 ));
151    }
152
153
154
155    /** Returns the thread access lock for this service.  Locking order: first lock the
156      * poll, then lock the trustserver.
157      *
158      *     @see VoteServer.Run#singleServiceLock()
159      */
160    public final ReentrantLock lock() { return lock; }
161
162
163        protected final ReentrantLock lock;
164
165
166
167    /** The local name of this service.  It must be unique among all voter services of the
168      * vote-server.  It must never change.
169      *
170      *     @see #NAME_MAX_LENGTH
171      *     @see #NAME_PATTERN
172      */
173    public @ThreadSafe final String name() { return name; }
174
175
176        protected final String name;
177
178
179
180    /** The maximum length of a service name.
181      *
182      *     @see #name()
183      *     @see <a href='http://reluk.ca/w/Category:Poll'
184      *                         >zelea.com/w/Category:Poll</a>
185      */
186    public static final int NAME_MAX_LENGTH = 50;
187
188
189
190    /** The allowable pattern of a service name.  The name may contain the ASCII letters
191      * (A-Z, a-z), digits (0-9), and the punctuation characters underscore (_), slash
192      * (/), period (.) and dash (-).  It must begin with a letter or an underscore, and
193      * must not end with a slash.  Double slashes are not allowed.
194      *
195      *     @see #name()
196      *     @see <a href='http://reluk.ca/w/Category:Poll'
197      *                         >zelea.com/w/Category:Poll</a>
198      */
199    public static final Pattern NAME_PATTERN =
200      Pattern.compile( "[A-Za-z_](?:/?[A-Za-z0-9_.\\-])*" ); // escaping the dash (\\-) is actually enough, no need to place it at end
201
202
203
204    /** The vote-server run, in which this service is provided.
205      */
206    public @ThreadSafe final VoteServer.Run vsRun() { return vsRun; }
207
208
209        protected final VoteServer.Run vsRun;
210
211
212
213    /** Returns the responder of a particular class name, or null if there is none.
214      */
215    public final CommandResponder responderByClassName( String className ) // by class name, rather than class, as a convenience because most lookups are by strings retrieved from the localized resource bundle
216    {
217        assert lock.isHeldByCurrentThread();
218        return respondersByClassName.get( className );
219    }
220
221
222        /** Map of responders, keyed by class name.
223          */
224        private Map<String,CommandResponder> respondersByClassName; // final after init()
225
226
227
228    /** Returns the responder for the specified command, or null if there is none.
229      *
230      *     @param argArray array of command name and arguments,
231      *         per CommandResponder.respond(argv,session).
232      */
233    public final CommandResponder responderForCommand( final String[] argArray,
234      final CommandResponder.Session commandSession )
235    {
236        assert lock.isHeldByCurrentThread();
237        final String commandName = argArray[0];
238        String responderClassName = null;
239        try
240        {
241            responderClassName = commandSession.replyBuilder().bundle().getString(
242              "a.VoterService.className.noTrans(" + commandName + ")" );
243        }
244        catch( MissingResourceException x ) { logger.finer( "no such command class exists: " + x ); }
245
246        CommandResponder responder = null;
247        if( responderClassName != null )
248        {
249            responder = responderByClassName( responderClassName );
250            if( responder == null ) logger.finer( "service does not support command class '" + responderClassName + "'" );
251        }
252
253        return responder;
254    }
255
256
257
258    /** An array of all responders.
259      */
260 // public final List<CommandResponder> responders() { return responders; }
261    public final CommandResponder[] responders() { return responderArray.clone(); }
262
263
264     // private List<CommandResponder> responders;
265        private CommandResponder[] responderArray; // final after init()
266
267
268
269    /** The directory containing this service's configuration files.
270      */
271    public @ThreadSafe final File serviceDirectory()
272    {
273        return startupConfigurationFile().getParentFile();
274    }
275
276
277
278    /** The startup configuration file for this service.  The language is JavaScript.
279      * There are restrictions on the {@linkplain votorola.g.script.JavaScriptIncluder
280      * character encoding}.
281      */
282    public abstract @ThreadSafe File startupConfigurationFile();
283
284
285
286    /** A short description that summarizes this service.
287      *
288      *     @see <a href='http://reluk.ca/w/Property:Short_description'
289      *       >Pollwiki Property:Short_description</a>
290      */
291    public abstract String summaryDescription();
292
293
294
295    /** The title of this service in wiki-style title case.  In English, that typically
296      * means only the first letter of the leading word is capitalized.
297      */
298    public abstract String title();
299
300
301
302   // - O b j e c t ----------------------------------------------------------------------
303
304
305    /** Returns true iff o is a voter service of the same class with the same {@linkplain
306      * #name() name}.
307      */
308    public @Override @ThreadSafe final boolean equals( final Object o )
309    {
310        // cf. PollService.compareTo
311        if( o == null || !getClass().equals( o.getClass() )) return false;
312
313        return name.equals( ((VoterService)o).name );
314    }
315
316
317
318 // public @Override final int hashCode() { return serviceEmail.hashCode(); }
319
320
321
322   /** Returns the service {@linkplain #name() name}.
323     */
324   public @Override @ThreadSafe final String toString() { return name(); }
325
326
327
328   // ====================================================================================
329
330
331    /** A context for configuring a {@linkplain VoterService voter serivce}.
332      */
333    public static @ThreadSafe abstract class ConstructionContext
334    {
335
336        protected ConstructionContext( String name, final JavaScriptIncluder s )
337          throws IllegalNameException
338        {
339            this( name, s, NAME_PATTERN );
340        }
341
342
343        /** @param namePattern the allowable pattern, which may restrict but must not
344          *   extend {@linkplain #NAME_PATTERN NAME_PATTERN}.
345          */
346        protected ConstructionContext( String _name, final JavaScriptIncluder s,
347          final Pattern namePattern ) throws IllegalNameException
348        {
349            startupConfigurationFile = s.scriptFile();
350            name = _name;
351
352            if( name.length() > NAME_MAX_LENGTH ) throw new IllegalNameException( "service name \"" + name + "\" exceeds maximum length " + NAME_MAX_LENGTH );
353
354            final Matcher m = namePattern.matcher( name );
355            if( !m.matches() ) throw new IllegalNameException( "service name \"" + name + "\" does not match allowable pattern: " + namePattern );
356
357        }
358
359
360       // --------------------------------------------------------------------------------
361
362
363        /** @see VoterService#startupConfigurationFile()
364          */
365        public final File startupConfigurationFile() { return startupConfigurationFile; }
366
367
368            private final File startupConfigurationFile;
369
370
371        /** @see VoterService#name()
372          */
373        public final String name() { return name; }
374
375
376            private final String name;
377
378
379    }
380
381
382
383   // ====================================================================================
384
385
386    /** Thrown when a service with an illegal name is requested.
387      *
388      *     @see VoterService#name()
389      */
390    public static final class IllegalNameException extends VotorolaRuntimeException
391    {
392        public IllegalNameException( String message ) { super( message ); }
393    }
394
395
396
397   // ====================================================================================
398
399
400    /** Thrown when an unknown voter service is requested.
401      */
402    public static final class NoSuchServiceException extends MisconfigurationException
403    {
404
405        public NoSuchServiceException( String message, File filename )
406        {
407            super( message, filename );
408        }
409
410    }
411
412
413
414//// P r i v a t e ///////////////////////////////////////////////////////////////////////
415
416
417    /** Stored as a convenience for subclass initialization, may be nulled afterwards by
418      * subclass.
419      */
420    protected ConstructionContext constructionContext;
421
422
423
424    protected final void helpA( final CommandResponder.Session session )
425    {
426        helpA_1( session );
427        helpA_2( session );
428        helpA_3( session );
429    }
430
431
432        protected final void helpA_1( final CommandResponder.Session session )
433        {
434            assert lock.isHeldByCurrentThread();
435            final ReplyBuilder replyB = session.replyBuilder();
436            replyB.setWrapping( false );
437            final String title = title();
438            replyB.appendln( title );
439            for( int c = title.length(); c > 0; --c ) replyB.append( '=' );
440            replyB.appendlnn();
441
442            replyB.setWrapping( true );
443        }
444
445
446        protected void helpA_2( final CommandResponder.Session session )
447        {
448            assert lock.isHeldByCurrentThread();
449            final ReplyBuilder replyB = session.replyBuilder();
450            replyB.setWrapping( false );
451            replyB.indent( 4 );
452            replyB.lappend( "a.VoterService.help.reply.summary(1,2)",
453              session.bunA().l( "a.serviceType(" + getClass().getName() + ")" ),
454              session.voterInterface().serviceAccessDescriptor( VoterService.this ));
455            replyB.exdent( 4 ).appendlnn();
456            replyB.setWrapping( true );
457        }
458
459
460        protected final void helpA_3( final CommandResponder.Session session )
461        {
462            assert lock.isHeldByCurrentThread();
463            session.replyBuilder().appendlnn( summaryDescription() );
464        }
465
466
467
468    protected final void helpB( final CommandResponder.Session session )
469    {
470        assert lock.isHeldByCurrentThread();
471        final ReplyBuilder replyB = session.replyBuilder();
472        replyB.lappendlnn( "a.VoterService.help.reply.body" );
473        replyB.indent( 4 );
474        replyB.lappend( "a.VoterService.help.reply.body-legend" );
475        replyB.exdent( 4 ).appendlnn();
476    }
477
478
479
480    protected final void helpC( final CommandResponder.Session session )
481    {
482        assert lock.isHeldByCurrentThread();
483        final ReplyBuilder replyB = session.replyBuilder();
484        final CommandResponder[] responderArrayCopy = responders();
485        Arrays.sort( responderArrayCopy, session.new ResponderNameComparator() );
486        for( CommandResponder responder: responderArrayCopy )
487        {
488            String title = responder.commandName( session );
489            replyB.appendln( title );
490            for( int c = title.length(); c > 0; --c ) replyB.append( '-' );
491            replyB.append( '\n' );
492            replyB.indent( 4 );
493            responder.help( session );
494            replyB.exdent( 4 );
495        }
496
497    }
498
499
500
501    private static final Logger logger = LoggerX.i( VoterService.class );
502
503
504}