package 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. import java.io.*; import java.net.*; import java.util.*; import java.util.concurrent.locks.*; import java.util.logging.*; import java.util.regex.*; import votorola.a.count.*; import votorola.a.response.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.option.*; import votorola.g.script.*; /** A facility for voters to access and maintain a category of data on a vote-server. */ public @ThreadRestricted("holds lock()") abstract class VoterService { /** Partially creates a VoterService. To complete it, call {@linkplain * #init(ArrayList) init}(responderList). */ protected VoterService( VoteServer.Run _vsRun, final ConstructionContext cc ) { vsRun = _vsRun; constructionContext = cc; name = cc.name; if( vsRun.isSingleThreaded() ) lock = vsRun.singleServiceLock(); else lock = new ReentrantLock(); } /** @param responderList listing only the service-specific responders; * the general responders will be added by this method. */ protected @ThreadRestricted("constructor") final void init( final ArrayList responderList ) { responderList.add( new CR_Hello( VoterService.this ) ); responderList.add( new CR_Help( VoterService.this )); responderList.add( new CR_Version( VoterService.this )); // responderList.trimToSize(); // responders = Collections.unmodifiableList( responderList ); responderArray = new CommandResponder[responderList.size()]; responderList.toArray( responderArray ); final int mapCapacity = (int)((responderList.size() + 1) / 0.75f) + 1; respondersByClassName = new HashMap( mapCapacity ); CommandResponder duplicate; for( CommandResponder responder: responderList ) { logger.finer( "adding responder for '" + name + "': " + responder.getClass().getName() ); duplicate = respondersByClassName.put( responder.getClass().getName(), responder ); assert duplicate == null : "single responder of class:" + responder.getClass(); } } // - V o t e r - S e r v i c e -------------------------------------------------------- /** Looks up the responder of the specified command, and sends the command * to it. Or, if the look-up fails, replies that the command is unrecognized. * * @param argArray an array containing the command name and arguments, * per CommandResponder.respond(argv,session). * @return any soft exception, per CommandResponder.respond(argv,session); * or null if none occured. * * @see CommandResponder#respond(String[],CommandResponder.Session) */ public Exception dispatch( final String[] argArray, final CommandResponder.Session commandSession ) { return dispatch( argArray, commandSession, responderForCommand( argArray, commandSession )); } /** Sends a command to its responder, if one is specified. Or, if none is specified, * replies that the command is unrecognized. * * @param argArray an array containing the command name and arguments, * per CommandResponder.respond(argv,session). * @param responder the responder for the command, or null if there is none. * * @return any soft exception, per CommandResponder.respond(argv,session); or * null if none occured. * * @see #responderForCommand(String[],CommandResponder.Session) * @see CommandResponder#respond(String[],CommandResponder.Session) */ public final Exception dispatch( final String[] argArray, final CommandResponder.Session commandSession, final CommandResponder responder ) { assert lock.isHeldByCurrentThread(); if( responder != null ) return responder.respond( argArray, commandSession ); final ReplyBuilder replyB = commandSession.replyBuilder(); final String commandName = argArray[0]; replyB.lappend( "a.VoterService.unrecognized(1)", commandName ); final String unrecognizedHelpKey = "a.VoterService.unrecognizedHelp"; if( !commandSession.containsKey( unrecognizedHelpKey )) // not yet prompted { commandSession.put( unrecognizedHelpKey, Boolean.TRUE ); final CommandResponder help = responderByClassName( CR_Help.class.getName() ); if( help != null ) { replyB.append( " " ); replyB.lappendlnn( unrecognizedHelpKey ); replyB.indent( 4 ); replyB.append( help.commandName( commandSession )); replyB.exdent( 4 ); } } replyB.appendlnn(); return null; } /** Responds to a help command on behalf of the nominal responder, per {@linkplain * CR_Help#respond(String[],Session) respond}(argv,session). */ public Exception help( final String[] argv, final CommandResponder.Session session ) { helpA( session ); helpB( session ); helpC( session ); return null; } /** Answers whether the named service is (or would be) a non-poll service. Currently * the names of non-poll services always begin with a lowercase letter, whereas the * names of polls never do. This is not guaranteed to hold in future, but you are * safe so long as this is your test method. */ public static boolean isNonPoll( String name ) { return Character.isLowerCase( name.codePointAt( 0 )); } /** Returns the thread access lock for this service. Locking order: first lock the * poll, then lock the trustserver. * * @see VoteServer.Run#singleServiceLock() */ public final ReentrantLock lock() { return lock; } protected final ReentrantLock lock; /** The local name of this service. It must be unique among all voter services of the * vote-server. It must never change. * * @see #NAME_MAX_LENGTH * @see #NAME_PATTERN */ public @ThreadSafe final String name() { return name; } protected final String name; /** The maximum length of a service name. * * @see #name() * @see zelea.com/w/Category:Poll */ public static final int NAME_MAX_LENGTH = 50; /** The allowable pattern of a service name. The name may contain the ASCII letters * (A-Z, a-z), digits (0-9), and the punctuation characters underscore (_), slash * (/), period (.) and dash (-). It must begin with a letter or an underscore, and * must not end with a slash. Double slashes are not allowed. * * @see #name() * @see zelea.com/w/Category:Poll */ public static final Pattern NAME_PATTERN = Pattern.compile( "[A-Za-z_](?:/?[A-Za-z0-9_.\\-])*" ); // escaping the dash (\\-) is actually enough, no need to place it at end /** The vote-server run, in which this service is provided. */ public @ThreadSafe final VoteServer.Run vsRun() { return vsRun; } protected final VoteServer.Run vsRun; /** Returns the responder of a particular class name, or null if there is none. */ 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 { assert lock.isHeldByCurrentThread(); return respondersByClassName.get( className ); } /** Map of responders, keyed by class name. */ private Map respondersByClassName; // final after init() /** Returns the responder for the specified command, or null if there is none. * * @param argArray array of command name and arguments, * per CommandResponder.respond(argv,session). */ public final CommandResponder responderForCommand( final String[] argArray, final CommandResponder.Session commandSession ) { assert lock.isHeldByCurrentThread(); final String commandName = argArray[0]; String responderClassName = null; try { responderClassName = commandSession.replyBuilder().bundle().getString( "a.VoterService.className.noTrans(" + commandName + ")" ); } catch( MissingResourceException x ) { logger.finer( "no such command class exists: " + x ); } CommandResponder responder = null; if( responderClassName != null ) { responder = responderByClassName( responderClassName ); if( responder == null ) logger.finer( "service does not support command class '" + responderClassName + "'" ); } return responder; } /** An array of all responders. */ // public final List responders() { return responders; } public final CommandResponder[] responders() { return responderArray.clone(); } // private List responders; private CommandResponder[] responderArray; // final after init() /** The directory containing this service's configuration files. */ public @ThreadSafe final File serviceDirectory() { return startupConfigurationFile().getParentFile(); } /** The startup configuration file for this service. The language is JavaScript. * There are restrictions on the {@linkplain votorola.g.script.JavaScriptIncluder * character encoding}. */ public abstract @ThreadSafe File startupConfigurationFile(); /** A short description that summarizes this service. * * @see Pollwiki Property:Short_description */ public abstract String summaryDescription(); /** The title of this service in wiki-style title case. In English, that typically * means only the first letter of the leading word is capitalized. */ public abstract String title(); // - O b j e c t ---------------------------------------------------------------------- /** Returns true iff o is a voter service of the same class with the same {@linkplain * #name() name}. */ public @Override @ThreadSafe final boolean equals( final Object o ) { // cf. PollService.compareTo if( o == null || !getClass().equals( o.getClass() )) return false; return name.equals( ((VoterService)o).name ); } // public @Override final int hashCode() { return serviceEmail.hashCode(); } /** Returns the service {@linkplain #name() name}. */ public @Override @ThreadSafe final String toString() { return name(); } // ==================================================================================== /** A context for configuring a {@linkplain VoterService voter serivce}. */ public static @ThreadSafe abstract class ConstructionContext { protected ConstructionContext( String name, final JavaScriptIncluder s ) throws IllegalNameException { this( name, s, NAME_PATTERN ); } /** @param namePattern the allowable pattern, which may restrict but must not * extend {@linkplain #NAME_PATTERN NAME_PATTERN}. */ protected ConstructionContext( String _name, final JavaScriptIncluder s, final Pattern namePattern ) throws IllegalNameException { startupConfigurationFile = s.scriptFile(); name = _name; if( name.length() > NAME_MAX_LENGTH ) throw new IllegalNameException( "service name \"" + name + "\" exceeds maximum length " + NAME_MAX_LENGTH ); final Matcher m = namePattern.matcher( name ); if( !m.matches() ) throw new IllegalNameException( "service name \"" + name + "\" does not match allowable pattern: " + namePattern ); } // -------------------------------------------------------------------------------- /** @see VoterService#startupConfigurationFile() */ public final File startupConfigurationFile() { return startupConfigurationFile; } private final File startupConfigurationFile; /** @see VoterService#name() */ public final String name() { return name; } private final String name; } // ==================================================================================== /** Thrown when a service with an illegal name is requested. * * @see VoterService#name() */ public static final class IllegalNameException extends VotorolaRuntimeException { public IllegalNameException( String message ) { super( message ); } } // ==================================================================================== /** Thrown when an unknown voter service is requested. */ public static final class NoSuchServiceException extends MisconfigurationException { public NoSuchServiceException( String message, File filename ) { super( message, filename ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Stored as a convenience for subclass initialization, may be nulled afterwards by * subclass. */ protected ConstructionContext constructionContext; protected final void helpA( final CommandResponder.Session session ) { helpA_1( session ); helpA_2( session ); helpA_3( session ); } protected final void helpA_1( final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); final ReplyBuilder replyB = session.replyBuilder(); replyB.setWrapping( false ); final String title = title(); replyB.appendln( title ); for( int c = title.length(); c > 0; --c ) replyB.append( '=' ); replyB.appendlnn(); replyB.setWrapping( true ); } protected void helpA_2( final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); final ReplyBuilder replyB = session.replyBuilder(); replyB.setWrapping( false ); replyB.indent( 4 ); replyB.lappend( "a.VoterService.help.reply.summary(1,2)", session.bunA().l( "a.serviceType(" + getClass().getName() + ")" ), session.voterInterface().serviceAccessDescriptor( VoterService.this )); replyB.exdent( 4 ).appendlnn(); replyB.setWrapping( true ); } protected final void helpA_3( final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); session.replyBuilder().appendlnn( summaryDescription() ); } protected final void helpB( final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); final ReplyBuilder replyB = session.replyBuilder(); replyB.lappendlnn( "a.VoterService.help.reply.body" ); replyB.indent( 4 ); replyB.lappend( "a.VoterService.help.reply.body-legend" ); replyB.exdent( 4 ).appendlnn(); } protected final void helpC( final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); final ReplyBuilder replyB = session.replyBuilder(); final CommandResponder[] responderArrayCopy = responders(); Arrays.sort( responderArrayCopy, session.new ResponderNameComparator() ); for( CommandResponder responder: responderArrayCopy ) { String title = responder.commandName( session ); replyB.appendln( title ); for( int c = title.length(); c > 0; --c ) replyB.append( '-' ); replyB.append( '\n' ); replyB.indent( 4 ); responder.help( session ); replyB.exdent( 4 ); } } private static final Logger logger = LoggerX.i( VoterService.class ); }