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 java.io.*; import java.sql.*; import java.util.*; import java.util.concurrent.locks.*; import java.util.logging.*; import javax.mail.internet.*; import votorola.a.election.*; import votorola.a.register.*; import votorola.a.voter.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.option.*; import votorola.g.sql.*; /** A facility for voters to access and maintain * a specific class of electoral data on a subserver. */ public @ThreadRestricted("holds lock()") abstract class ElectoralService { /** Partially creates an ElectoralService. To complete it, * call {@linkplain #init(ArrayList) init}(responderList). * * @param configurationContext per {@linkplain #configurationContext configurationContext} */ protected ElectoralService( ElectoralSubserver.Run subserverRun, String name, Object configurationContext ) { this.subserverRun = subserverRun; this.name = name; this.configurationContext = configurationContext; serviceEmail = name + '@' + subserverRun.subserver().domainName(); if( subserverRun.isSingleThreaded() ) lock = subserverRun.singleServiceLock(); else lock = new ReentrantLock(); } /** @param responderList listing only the service-specific responders * (general responders will be added here) */ protected @ThreadRestricted("constructor") final void init( final ArrayList responderList ) { responderList.add( new CR_Hello( ElectoralService.this ) ); responderList.add( new CR_Help( ElectoralService.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 " + serviceEmail + ": " + responder.getClass().getName() ); duplicate = respondersByClassName.put( responder.getClass().getName(), responder ); assert duplicate == null : "single responder of class:" + responder.getClass(); } } // - E l e c t o r a l - S e r v i c e ------------------------------------------------ /** The primary configuration file for this electoral service. For an ordinary * (non-meta) service, it is typically located at: *
* {@linkplain ElectoralSubserver#votorolaDirectory() votorola-directory}/services/{@linkplain #name() service-name}/service.js *
*

* The language is JavaScript. There are restrictions * on the {@linkplain votorola.g.script.JavaScriptIncluder character encoding}. *

*/ public abstract @ThreadSafe File configurationFile(); /** 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.ElectoralService.unrecognized(1)", commandName ); final String unrecognizedHelpKey = "a.ElectoralService.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; } /** Ensures the database has a schema for the specified electoral service, * creating it if necessary. */ public static @ThreadSafe void ensureSchema( final Database database, final String name ) throws SQLException { final String key = ElectoralService.class.getName() + ".ensureSchema"; synchronized( database ) { PreparedStatement s = database.statementCache().get( key ); if( s == null ) { s = database.connection().prepareStatement( "CREATE SCHEMA \"" + name + "\"" ); database.statementCache().put( key, s ); } try { s.execute(); } catch( SQLException x ) { if( !"42P06".equals( x.getSQLState() )) throw x; } // DUPLICATE SCHEMA } } /** 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 ) { final ReplyBuilder replyB = session.replyBuilder(); helpA( replyB, session ); helpB( replyB, session ); helpC( replyB, session ); return null; } /** Returns the thread access lock for this service. * Locking order: first lock the election, then lock the register. * * @see ElectoralSubserver.Run#singleServiceLock() */ public final ReentrantLock lock() { return lock; } protected final ReentrantLock lock; /** The name that identifies this service, uniquely among all services on the * subserver. It must be valid as the local part (before the '@') of an email * address, in unquoted (dot-atom) form. It may therefore contain ASCII * alpha-numeric characters (A-Z, a-z, 0-9), and most punctuation characters (!, #, * $, %, &, ', *, +, -, /, =, ?, ^, _, `, {, |, }, ~ ). It may also include a * period (.), but may not begin with one. However, it is recommended that all names * be restricted to the common characters (A-Z, a-z, 0-9, -, _) and dot (., except no * dot as the first character). *

* The name must not exceed 63 characters in length. This is a PostgreSQL limit. * (The next limit would be to 'avoid local parts [of email addresses] * longer than 64 characters', per qmail addresses(5).) *

*

* The name ought to be unique among all services of the subserver, * and all other subservers that share the same * {@linkplain ElectoralSubserver#domainName() domain name}. *

* * @see #serviceEmail() */ public @ThreadSafe final String name() { return name; } protected final String name; /** 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.ElectoralService.className{" + 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; } /** Returns 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 email address that identifies this service. It is constructed * from the service name and subserver domain name, * as '{@linkplain #name() name}@{@linkplain ElectoralSubserver#domainName() domainName}'. * * @return canonical email address (having no personal part) */ public @ThreadSafe String serviceEmail() { return serviceEmail; } protected final String serviceEmail; /** The subserver run, in which this service is provided. */ public @ThreadSafe final ElectoralSubserver.Run subserverRun() { return subserverRun; } protected final ElectoralSubserver.Run subserverRun; /** A brief description of this service, in sentence form. * It is intended for display, for example, as an introductory paragraph. */ public abstract String summaryDescription(); /** The title of this service, in title case. */ public abstract String title(); // - O b j e c t ---------------------------------------------------------------------- /** Returns true iff o is an electoral service with the same * {@linkplain #serviceEmail() service email address}. */ public @Override @ThreadSafe final boolean equals( Object o ) { if( o == null || !getClass().equals( o.getClass() )) return false; return serviceEmail.equals( ((ElectoralService)o).serviceEmail ); } // public @Override final int hashCode() { return serviceEmail.hashCode(); } /** Returns the {@linkplain #serviceEmail() service email address}. */ public @Override @ThreadSafe final String toString() { return serviceEmail(); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** Stored as a convenience for subclass initialization, * may be nulled afterwards by subclass. */ protected Object configurationContext; protected final void helpA( ReplyBuilder replyB, final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); String serviceAddressString; try { serviceAddressString = new InternetAddress( serviceEmail, title() ) .toUnicodeString(); } catch( UnsupportedEncodingException x ) { assert false; serviceAddressString = serviceEmail; } replyB.setWrapping( false ).appendln( serviceAddressString ); int length = title().length(); if( serviceAddressString.charAt(0) != title().charAt(0) ) length += 2; // quoted personal part (title), so make room for quotes for( int c = length; c > 0; --c ) replyB.append( '=' ); replyB.appendlnn().setWrapping( true ); replyB.appendlnn( summaryDescription() ); } protected final void helpB( ReplyBuilder replyB, final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); replyB.lappendlnn( "a.ElectoralService.help.reply.body(1)", name ); } protected final void helpC( ReplyBuilder replyB, final CommandResponder.Session session ) { assert lock.isHeldByCurrentThread(); 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( ElectoralService.class ); }