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.atomic.*; import java.util.concurrent.locks.*; import java.util.logging.*; import javax.mail.internet.*; import javax.script.*; import org.postgresql.ds.*; import votorola._.*; import votorola.a.election.*; import votorola.a.election.district.*; import votorola.a.register.*; import votorola.a.voter.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.script.*; import votorola.g.sql.*; import votorola.g.util.*; /** An electoral subserver. * * @see design.xht#electoral-subserver */ public @ThreadSafe final class ElectoralSubserver { /** Constructs an ElectoralSubserver. * * @param name per {@linkplain #name() name}() */ public ElectoralSubserver( final String name ) throws ScriptException { this.name = name; servicesDirectory = new File( File.separator + "home" + File.separator + name + File.separatorChar + "votorola" + File.separatorChar + "services" ); configurationFile = new File( votorolaDirectory(), "subserver.js" ); final ConfigurationContext cc = ConfigurationContext.configure( name, new JavaScriptIncluder( configurationFile )); cacheDirectory = new File( cc.getCacheDirectoryPath() ); geocodeDatabase = new Database( cc.geocodeDatabase() ); registerName = cc.getRegisterName(); serverName = cc.getServerName(); shortTitle = cc.getShortTitle(); summaryDescription = cc.getSummaryDescription(); title = cc.getTitle(); userDatabase = new Database( cc.userDatabase() ); } // ------------------------------------------------------------------------------------ /** The directory for storage of files that are generated at runtime, * and persisted from run to run. The directory is created at runtime * if it does not already exist. * * @see ConfigurationContext#setCacheDirectoryPath(String) * @see votorola.a.web.VApplication#cacheDirectory() */ public File cacheDirectory() { return cacheDirectory; } private final File cacheDirectory; /** The configuration file for this subserver. It is located at: *
* {@linkplain #votorolaDirectory() votorolaDirectory}/subserver.js **
* The language is JavaScript. There are restrictions * on the {@linkplain votorola.g.script.JavaScriptIncluder character encoding}. *
*/ File configurationFile() { return configurationFile; } private final File configurationFile; /** The name of the user account on the local host, that identifies this subserver. * * @see a/design.xht#electoral-subserver-account */ public String name() { return name; } private final String name; /** The {@linkplain ElectoralService#name service name} * that identifies the electoral register for this subserver. * * @see Run#register() * @see ConfigurationContext#setRegisterName(String) */ public String registerName() { return registerName; } private final String registerName; /** The fully qualified name of the server on which this subserver is hosted. It * includes the Internet domain name. For example: "www.mydomain.dom". * * @see ConfigurationContext#setServerName(String) */ public String serverName() { return serverName; } private final String serverName; /** The directory of electoral services that are automatically launched for each run * of the subserver. (In future, multiple directories might be configured. * But for now, it's just this one.) It is located at: ** {@linkplain #votorolaDirectory() votorolaDirectory}/services **/ public File servicesDirectory() { return servicesDirectory; } private final File servicesDirectory; /** A short version of the {@linkplain #title() title}, * restricted to roughly {@linkplain votorola.a.web.VPage#SHORT_STRING_LENGTH_MAX * SHORT_STRING_LENGTH_MAX} characters. * * @see #title() * @see ConfigurationContext#setShortTitle(String) */ public String shortTitle() { return shortTitle; } private final String shortTitle; /** A brief description of this electoral subserver, in sentence form. * It is intended for display, for example, as an introductory paragraph. * * @see ConfigurationContext#setSummaryDescription(String) */ public String summaryDescription() { return summaryDescription; } private final String summaryDescription; /** The title of this subserver, in title case. Normally it includes * the informal name of the subserver's overall district (town or region, for example). * * @see #shortTitle() * @see ConfigurationContext#setTitle(String) */ public String title() { return title; } private final String title; /** The root directory (/home/{@linkplain #name() subserver-name}/votorola) * of all subserver configuration files. */ public File votorolaDirectory() { return servicesDirectory.getParentFile(); } // ==================================================================================== /** A context for configuring an {@linkplain ElectoralSubserver electoral subserver}. * The subserver is configured by its {@linkplain * ElectoralSubserver#configurationFile() configuration file}, which contains a * script (s) for that purpose. During construction of the subserver, an instance of * this context (subserverCC) is passed to s, via s::configureSubserver(subserverCC). */ public static @ThreadSafe final class ConfigurationContext // public class and getters, accessible by configuration scripts { /** Constructs the complete configuration of the subserver. * * @param name the subserver name * @param s the compiled configuration script */ private static ConfigurationContext configure( final String name, final JavaScriptIncluder s ) throws ScriptException { final ConfigurationContext cc = new ConfigurationContext( name, s ); s.invokeKnownFunction( "configureSubserver", cc ); if( cc.getServerName() == null ) throw new MisconfigurationException( "null server name, please configure using setServerName", cc.configurationFile ); return cc; } private ConfigurationContext( final String name, final JavaScriptIncluder s ) { this.name = name; configurationFile = s.scriptFile(); // cacheDirectoryPath = System.getProperty("java.io.tmpdir") + File.separator + name; //// no, because Tomcat sets that property to a dir that's writeable only by it cacheDirectoryPath = File.separator + "tmp" + File.separator + "votorola" + File.separator + name; geocodeDatabase = new DatabaseCC( name ); summaryDescription = "This is an electoral subserver. Further information " + "is unavailable, because the 'configureSubserver' function " + "of script " + configurationFile + " " + "makes no call to 'setSummaryDescription'."; userDatabase = new DatabaseCC( name ); } private final File configurationFile; // -------------------------------------------------------------------------------- /** The context for configuring the database of the subserver's * {@linkplain ElectoralSubserver.Run#geocodeTable() geocode table}. * At runtime, a table named "geocode" is created in this database, * if it does not already exist. */ public DatabaseCC geocodeDatabase() { return geocodeDatabase; } private final DatabaseCC geocodeDatabase; /** @see ElectoralSubserver#cacheDirectory() * @see #setCacheDirectoryPath(String) */ public String getCacheDirectoryPath() { return cacheDirectoryPath; } private String cacheDirectoryPath; /** Sets the path of the cache directory for the subserver. * The default value is: *
* "/tmp/votorola/{@linkplain #name() name}" ** * @see ElectoralSubserver#cacheDirectory() */ @ThreadRestricted("constructor") public void setCacheDirectoryPath( String cacheDirectoryPath ) { this.cacheDirectoryPath = cacheDirectoryPath; } /** @see ElectoralSubserver#serverName() * @see #setServerName(String) */ public String getServerName() { return serverName; } private String serverName; /** Sets the name of the server on which this subserver is hosted. * There is no default, this value must be set. * * @see ElectoralSubserver#serverName() */ @ThreadRestricted("constructor") public void setServerName( String serverName ) { this.serverName = serverName; } /** @see #getTitle() */ public @Deprecated String getDistrictName() { return title; } /** @see #setTitle(String) */ @Deprecated @ThreadRestricted("constructor") public void setDistrictName( String title ) { this.title = title; } /** @see ElectoralSubserver#registerName() * @see #setRegisterName(String) */ public String getRegisterName() { return registerName; } private String registerName = "test-register"; /** Sets the name that identifies the electoral register. * The default value is "test-register". * * @see ElectoralSubserver#registerName() */ @ThreadRestricted("constructor") public void setRegisterName( String registerName ) { this.registerName = registerName; } /** @see #getShortTitle() */ public @Deprecated String getShortDistrictName() { return shortTitle; } /** @see #setShortTitle(String) */ @Deprecated @ThreadRestricted("constructor") public void setShortDistrictName( String shortTitle ) { this.shortTitle = shortTitle; } /** @see ElectoralSubserver#shortTitle() * @see #setShortTitle(String) */ public String getShortTitle() { return shortTitle; } private String shortTitle = "Subserver"; /** Sets the short title of the subserver. The default value is "Subserver". * * @see ElectoralSubserver#shortTitle() */ @ThreadRestricted("constructor") public void setShortTitle( String shortTitle ) { this.shortTitle = shortTitle; } /** @see ElectoralSubserver#summaryDescription() * @see #setSummaryDescription(String) */ public String getSummaryDescription() { return summaryDescription; } private String summaryDescription; /** Sets the summary description of the electoral subserver. * The default value is a placeholder with configuration instructions * for the administrator. * * @throws IllegalArgumentException if the description contains * any newline characters, because they might render inconsistently * across different types of user interface * @see ElectoralSubserver#summaryDescription() */ @ThreadRestricted("constructor") public void setSummaryDescription( String summaryDescription ) { if( summaryDescription.indexOf('\n') >= 0 ) throw new IllegalArgumentException( "argument contains a newline character" ); this.summaryDescription = summaryDescription; } /** @see ElectoralSubserver#title() * @see #setTitle(String) */ public String getTitle() { return title; } private String title = "Electoral Subserver"; /** Sets the title of the subserver. The default value is "Electoral Subserver". * * @see ElectoralSubserver#title() */ @ThreadRestricted("constructor") public void setTitle( String title ) { this.title = title; } /** The name of the subserver. * * @see ElectoralSubserver#name() */ public String name() { return name; } private final String name; /** The context for configuring the database of the subserver's * {@linkplain ElectoralSubserver.Run#userTable() user table}. * At runtime, a table named "user" is created in this database, * if it does not already exist. */ public DatabaseCC userDatabase() { return userDatabase; } private final DatabaseCC userDatabase; } // ================================================================================ /** A context for configuring a PostgreSQL database for an electoral subserver. */ public static @ThreadSafe final class DatabaseCC extends Database.ConfigurationContext { /** Constructs a DatabaseCC. */ public DatabaseCC( final String subserverName ) { name = subserverName; userName = subserverName; } /** @see #setName(String) */ public @Override String getName() { return name; } private String name; /** Sets the name of the database. The default value is * the {@linkplain ElectoralSubserver#name() subserver name}. * * @see #getName() */ @ThreadRestricted("constructor") public void setName( String name ) { this.name = name; } /** @see #setUserName(String) */ public @Override String getUserName() { return userName; } private String userName; /** Sets the database user name. The default value is * the {@linkplain ElectoralSubserver#name() subserver user name}. * * @see #getUserName() */ @ThreadRestricted("constructor") public void setUserName( String userName ) { this.userName = userName; } } // ==================================================================================== /** Thrown when an unknown electoral service is requested. */ public static final class NoSuchServiceException extends MisconfigurationException { public NoSuchServiceException( String message, File filename ) { super( message, filename ); } } // ==================================================================================== /** A run of the electoral subserver. */ public @ThreadSafe final class Run { /** Constructs a Run. * * @param isSingleThreaded per {@linkplain #isSingleThreaded() isSingleThreaded}() - * if false (multi-threaded), the client should call * {@linkplain #init_done init_done}() after initialization is complete */ public Run( final boolean isSingleThreaded ) throws IOException, ScriptException, SQLException { if( isSingleThreaded ) singleServiceLock = new ReentrantLock(); else singleServiceLock = null; if( !cacheDirectory.isDirectory() ) { final String userName = System.getProperty( "user.name" ); if( name.equals( userName )) { if( !cacheDirectory.mkdirs() ) throw new IOException( "unable to create subserver cache directory " + cacheDirectory.getPath() ); } else throw new IOException( "unable to create subserver cache directory " + cacheDirectory + ", because current user '" + userName + "' is not the subserver account user '" + name + "'" ); // avoid creating directory having wrong owner } { // try // { geocodeDatabase = getOrInitializeDatabase( geocodeDatabase ); // } // finally{ geocodeDatabase.logAndClearWarnings(); } // to show SQLState //// but no warnings recorded there geocodeTable = new Geocode.Table( geocodeDatabase ); geocodeDatabase = null; // nobody else needs it } { // try // { userDatabase = getOrInitializeDatabase( userDatabase ); // } // finally{ userDatabase.logAndClearWarnings(); } // to show SQLState //// but no warnings recorded there userTable = new UserSettings.Table( userDatabase ); userDatabase = null; // nobody else needs it } try { register = (Register)init_ensureElectoralService( registerName, Register.class ); } catch( ScriptException x ) { throw new MisconfigurationException( "unable to initialize electoral register: " + registerName, configurationFile, x ); } } private final AtomicReference