package votorola.a; // Copyright 2007-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.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.a.diff.*; import votorola.s.mail.*; import votorola.a.count.*; import votorola.a.trust.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.net.*; import votorola.g.script.*; import votorola.g.sql.*; /** A server that provides polls and other voter services. * * @see ../s/manual.xht#vote-server */ public @ThreadSafe final class VoteServer { /** Constructs a VoteServer. */ public VoteServer( final String name ) throws IOException, ScriptException, URISyntaxException { this.name = name; votorolaDirectory = new File( BASH.homeDirectory(name), "votorola" ); // not relying on property user.home, as user might not be vote-server startupConfigurationFile = new File( votorolaDirectory, "vote-server.js" ); final ConstructionContext cc = ConstructionContext.configure( name, votorolaDirectory, new JavaScriptIncluder( startupConfigurationFile )); serverName = cc.getServerName(); shortTitle = cc.getShortTitle(); summaryDescription = cc.getSummaryDescription(); testUseMode = cc.getTestUseMode(); title = cc.getTitle(); votorolaURI = cc.getVotorolaURI(); codeDirectory = new File( votorolaDirectory(), "code" ); inDirectory = new File( votorolaDirectory(), "in" ); outDirectory = new File( votorolaDirectory(), "out" ); database = new Database( cc.database() ); diffCache = new DiffCache( VoteServer.this, cc ); pollwiki = new PollwikiVS( cc.pollwiki() ); pollwiki.init( VoteServer.this ); scopeCount = new Count.VoteServerScope( VoteServer.this ); scopePoll = new PollService.VoteServerScope( VoteServer.this, cc ); scopeTrace = new NetworkTrace.VoteServerScope( VoteServer.this ); } // ------------------------------------------------------------------------------------ /** The directory ~/votorola/code in which vote-server's runtime code is * stored. */ public File codeDirectory() { return codeDirectory; } private final File codeDirectory; /** The cache of diff output. */ public DiffCache diffCache() { return diffCache; } private final DiffCache diffCache; /** The base directory ~/votorola/in of the file portion of the input * store. It serves mostly as a cache of files fetched from external sources. * * @see InputStore */ public File inDirectory() { return inDirectory; } private final File inDirectory; /** The login name of the local host account, under which the vote-server's data is * stored and its processes are run. * * @see #serverName() * @see ../s/manual.xht#vote-server-name */ public String name() { return name; } private final String name; /** The base directory ~/votorola/out of the file portion of the output * store. * * @see OutputStore */ public File outDirectory() { return outDirectory; } private final File outDirectory; /** The pollwiki associated with this vote-server. */ public PollwikiVS pollwiki() { return pollwiki; } private final PollwikiVS pollwiki; /** API for all counts within the scope of this vote-server. */ public Count.VoteServerScope scopeCount() { return scopeCount; } private final Count.VoteServerScope scopeCount; /** API for all polls within the scope of this vote-server. */ public PollService.VoteServerScope scopePoll() { return scopePoll; } private final PollService.VoteServerScope scopePoll; /** API for all traces of trust networks within the scope of this vote-server. */ public NetworkTrace.VoteServerScope scopeTrace() { return scopeTrace; } private final NetworkTrace.VoteServerScope scopeTrace; /** The name of the computer on which this vote-server is hosted. For example * "localhost", "hostname" or (fully qualified) "hostname.mydomain.dom". It is used * to construct service and return email addresses for the various "serviceEmail" methods. It is also used * to construct the default URL to the wiki. In a public facing server, this should * be a fully qualified name. * * @see #name() * @see ConstructionContext#setServerName(String) */ public String serverName() { return serverName; } private final String serverName; /** A short version of the {@linkplain #title() title}, restricted to roughly * {@linkplain votorola.a.web.wic.VPage#SHORT_STRING_LENGTH_MAX SHORT_STRING_LENGTH_MAX} * characters. For example, "Toronto". * * @see #title() * @see ConstructionContext#setTitle(String,String) */ public String shortTitle() { return shortTitle; } private final String shortTitle; /** The startup configuration file for this vote-server. It is located at: * *

{@linkplain #votorolaDirectory() * votorolaDirectory}/vote-server.js

* *

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

*/ File startupConfigurationFile() { return startupConfigurationFile; } private final File startupConfigurationFile; /** A brief description of this vote-server, in sentence form. * It is intended for display, for example, as an introductory paragraph. * * @see ConstructionContext#setSummaryDescription(String) */ public String summaryDescription() { return summaryDescription; } private final String summaryDescription; /** The test use mode of this vote-server, which determines whether users may log in * under aliases for test purposes. * * @see ConstructionContext#setTestUseMode(VoteServer.TestUseMode) */ public TestUseMode testUseMode() { return testUseMode; } private final TestUseMode testUseMode; /** The title of this vote-server, in wiki-style (first word only) title case. If the * vote-server serves a single deployment area, such as a town or region, then it will * normally take the title of that area. For example, "City of Toronto". * * @see #shortTitle() * @see ConstructionContext#setTitle(String,String) * @see Deployment areas */ public String title() { return title; } private final String title; /** The base directory ~/votorola of all vote-server configuration files, * where ~ is the home directory of the vote-server account. */ public File votorolaDirectory() { return votorolaDirectory; } private final File votorolaDirectory; /** The public location of the {@linkplain #votorolaDirectory() votorola directory} * without a trailing slash (/), or a local "file" URI if the directory is * unpublished. Not all files within the directory are necessarily visible or * readable by everyone. * * @see ConstructionContext#setVotorolaLocation(String) * @see ConstructionContext#setVotorolaURI(URI) */ public URI votorolaURI() { return votorolaURI; } private final URI votorolaURI; // ==================================================================================== /** A context for configuring the construction of a {@linkplain VoteServer * VoteServer}. The construction is configured by the vote-server's {@linkplain * VoteServer#startupConfigurationFile() startup configuration file}, which contains * a script (s) for that purpose. During construction, an instance of this context * (vsCC) is passed to s via s::constructingVoteServer(vsCC). */ public static @ThreadSafe final class ConstructionContext { /** Constructs the complete configuration of the vote-server. * * @param _name the name of the vote-server. * @param s the compiled startup configuration script. */ private static ConstructionContext configure( String _name, final File votorolaDirectory, final JavaScriptIncluder s ) throws ScriptException, URISyntaxException { final ConstructionContext cc = new ConstructionContext( _name, s ); s.invokeKnownFunction( "constructingVoteServer", cc ); if( cc.votorolaURI == null ) { final StringBuilder b = new StringBuilder( votorolaDirectory.toURI().toASCIIString() ); { final int cLast = b.length() - 1; if( b.charAt(cLast) == '/' ) b.deleteCharAt( cLast ); } cc.setVotorolaLocation( b.toString() ); } cc.pollwiki.postConfigure( cc ); return cc; } private ConstructionContext( final String name, final JavaScriptIncluder s ) { this.name = name; startupConfigurationFile = s.scriptFile(); database = new DatabaseCC( name ); summaryDescription = "This is a vote-server. Further information " + "is unavailable, because the 'constructingVoteServer' function " + "of script " + startupConfigurationFile + " " + "makes no call to 'setSummaryDescription'."; } private final File startupConfigurationFile; // -------------------------------------------------------------------------------- /** The context for configuring the vote-server's relational database. */ public DatabaseCC database() { return database; } private final DatabaseCC database; /** @see votorola.a.count.PollService.VoteServerScope#pollCacheCapacity() * @see #setPollCacheCapacity(int) */ public int getPollCacheCapacity() { return pollCacheCapacity; } private int pollCacheCapacity = 500; /** Sets the maximum number of entries in the poll cache. The default value * is 500 polls. * * @see votorola.a.count.PollService.VoteServerScope#pollCacheCapacity() */ @ThreadRestricted("constructor") public void setPollCacheCapacity( int _pollCacheCapacity ) { pollCacheCapacity = _pollCacheCapacity; } /** @see VoteServer#serverName() * @see #setServerName(String) */ public String getServerName() { return serverName; } private String serverName; { serverName = "localhost"; try { final InetAddress a = InetAddress.getLocalHost(); final String name = a.getHostName(); if( name.equals( a.getHostAddress() )) { logger.config( "unable to resolve default serverName, reverting to 'localhost'" ); } else serverName = name; } catch( UnknownHostException x ) { logger.config( "unable to resolve default serverName, reverting to 'localhost': " + x ); } } /** Specifies the fully qualified name of the computer on which this * vote-server is hosted. The default value is the system hostname as * determined at runtime, or failing that "localhost". It is recommended * that you explicitly set "localhost" or a fully qualified name. * * @see VoteServer#serverName() */ @ThreadRestricted("constructor") public void setServerName( final String serverName ) { this.serverName = serverName; } // If not correctly set, redirection links such as "my draft" may fail to // convey state as expected. See logged warning in // votorola.s.wic.WP_Draft.maybeRedirectToDraft. /** @see VoteServer#shortTitle() * @see #setTitle(String,String) */ public String getShortTitle() { return shortTitle; } private String shortTitle = "Votorola"; /** @see VoteServer#summaryDescription() * @see #setSummaryDescription(String) */ public String getSummaryDescription() { return summaryDescription; } private String summaryDescription; /** Sets the summary description of the vote-server. 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 VoteServer#summaryDescription() */ @ThreadRestricted("constructor") public void setSummaryDescription( final String summaryDescription ) { if( summaryDescription.indexOf('\n') >= 0 ) throw new IllegalArgumentException( "argument contains a newline character" ); this.summaryDescription = summaryDescription; } /** @see VoteServer#testUseMode() * @see #setTestUseMode(VoteServer.TestUseMode) */ public TestUseMode getTestUseMode() { return testUseMode; } private TestUseMode testUseMode = TestUseMode.OFF; /** Sets the test use mode of the vote-server. The default value is * TestUseMode.OFF. * * @see VoteServer#testUseMode() */ @ThreadRestricted("constructor") public void setTestUseMode( final TestUseMode testUseMode ) { this.testUseMode = testUseMode; } /** @see VoteServer#title() * @see #setTitle(String,String) */ public String getTitle() { return title; } private String title = "Votorola vote-server"; /** Sets the title of the vote-server. The default values for the long and * short titles are "Votorola vote-server" and "Votorola". * * @see VoteServer#title() * @see VoteServer#shortTitle() */ @ThreadRestricted("constructor") public void setTitle( final String title, final String shortTitle ) { this.title = title; this.shortTitle = shortTitle; } /** The name of the vote-server. * * @see VoteServer#name() */ public String name() { return name; } private final String name; /** The context for configuring the vote-server's pollwiki. */ public PollwikiVS.ConstructionContext pollwiki() { return pollwiki; } private final PollwikiVS.ConstructionContext pollwiki = new PollwikiVS.ConstructionContext(); /** The public location of the {@linkplain VoteServer#votorolaDirectory() votorola * directory}, or null for the default. * * @see VoteServer#votorolaURI() * @see #setVotorolaLocation(String) * @see #setVotorolaURI(URI) */ public URI getVotorolaURI() { return votorolaURI; } private URI votorolaURI = null; /** Sets the public location of the {@linkplain VoteServer#votorolaDirectory() * votorola directory}. The default value is null, which translates at * runtime to a local "file" URI. * * @see VoteServer#votorolaURI() * @throws IllegalArgumentException if the URI ends with a slash '/' character. */ @ThreadRestricted("constructor") public void setVotorolaLocation( final String s ) throws URISyntaxException { setVotorolaURI( new URI( s )); } /** Sets the public location of the {@linkplain VoteServer#votorolaDirectory() * votorola directory}. The default value is null, which translates at * runtime to a local "file" URI. * * @see VoteServer#votorolaURI() * @throws IllegalArgumentException if the URI ends with a slash '/' character. */ public @ThreadRestricted("constructor") void setVotorolaURI( final URI u ) { if( u.toString().endsWith( "/" )) { throw new IllegalArgumentException( "URI ends with '/'" ); } votorolaURI = u; } } // ================================================================================ /** A context for configuring the construction of a vote-server {@linkplain Database * Database}. */ public static @ThreadSafe final class DatabaseCC extends Database.ConstructionContext { /** Constructs a DatabaseCC. */ public DatabaseCC( final String voteServerName ) { name = voteServerName; username = voteServerName; } /** @see #setName(String) */ public @Override String getName() { return name; } private String name; /** Sets the name of the database. The default value is the {@linkplain * VoteServer#name() vote-server 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 * VoteServer#name() vote-server name}. * * @see #getUsername() */ @ThreadRestricted("constructor") public void setUsername( String username ) { this.username = username; } } // ==================================================================================== /** A run of the vote-server. */ 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( !outDirectory.isDirectory() ) { throw new IOException( "vote-server output store, no such directory: " + outDirectory ); // fail fast // final String username = System.getProperty( "user.name" ); // if( name.equals( username )) // { // if( !outDirectory.mkdirs() ) throw new IOException( "unable to create vote-server output directory " + outDirectory.getPath() ); // } // else throw new IOException( "unable to create vote-server output directory " + outDirectory + ", because current user '" + username + "' is not the vote-server account user '" + name + "'" ); // avoid creating directory having wrong owner, such as tomcat } // try // { database.init( new PGSimpleDataSource() ); // } // finally{ database.logAndClearWarnings(); } // to show SQLState //// but no warnings recorded there geocodeTable = new Geocode.Table( database ); userTable = new UserSettings.Table( database ); try { trustserver = init_ensureVoterService( new File( votorolaDirectory(), "trust" + File.separatorChar + "trustserver.js" ), Trustserver.class ); } catch( ScriptException x ) { throw new MisconfigurationException( "unable to initialize trustserver", startupConfigurationFile, x ); } scopePoll = VoteServer.this.scopePoll.new Run( Run.this ); } public @Warning( "non-API" ) final AtomicReference constructionThreadA = new AtomicReference( Thread.currentThread() ); /** Records that initialization is complete - that no further calls will be made * to init_ methods. {@linkplain #isSingleThreaded() Single-threaded clients} * need not call this method; it only enables thread-safety assertions in the * init_ methods. [No longer true, as some late constructs will now throw an * IllegalStateException if this method has not been called.] */ public void init_done() { assert Thread.currentThread().equals( constructionThreadA.get() ); constructionThreadA.set( null ); } /** Returns a voter service, creating it if necessary and storing it for later * retrieval. * * @param startupConfigurationFile the startup configuration file for the service. * @param serviceClass the class of the service. * * @throws VoterService.NoSuchServiceException if startupConfigurationFile * does not exist. * * @see #init_putVoterService(VoterService) * @see #voterService(String) */ @ThreadRestricted("constructor") // so no worry about deadlock involving run and service locks held during atomic get/create/put of new service public S init_ensureVoterService( final File startupConfigurationFile, final Class serviceClass ) throws IOException, ScriptException, SQLException { assert Thread.currentThread().equals( constructionThreadA.get() ); VoterService s = voterService( startupConfigurationFile.getParentFile().getName() ); if( s == null ) { final JavaScriptIncluder iJS; { // logger.config( "loading configuration: " + startupConfigurationFile ); // catch-all disclosure, particularly for VOTer /// how is this useful? why not do same for all config files? if( !startupConfigurationFile.isFile() ) throw new VoterService.NoSuchServiceException( "missing startup configuration file", startupConfigurationFile ); iJS = new JavaScriptIncluder( startupConfigurationFile ); } // if( serviceClass.equals( PollService.class )) s = scopePoll.newPoll( iJS ); if( serviceClass.equals( PollService.class )) throw new IllegalArgumentException( "attempt to construct poll, use scopePoll.ensurePoll() instead" ); else if( serviceClass.equals( Trustserver.class )) s = Trustserver.newTrustserver( Run.this, iJS ); else if( serviceClass.equals( MailMetaService.class )) s = MailMetaService.newMetaService( Run.this, iJS ); else throw new IllegalArgumentException( "unknown service type: " + serviceClass ); init_putVoterService( s ); } return serviceClass.cast( s ); } /** Stores a voter service in this run, for later retrieval. * * @throws IllegalStateException if an instance * of the same service was already stored. * * @see #voterService(String) */ @ThreadRestricted("constructor") public void init_putVoterService( VoterService service ) { assert Thread.currentThread().equals( constructionThreadA.get() ); if( voterServiceMap.put( service.name(), service ) != null ) throw new IllegalStateException( "duplicate service: " + service ); } // -------------------------------------------------------------------------------- /** The vote-server's relational database. * * @see ConstructionContext#database() */ public @Warning("thread restricted object") Database database() { return database; } // OPT this bottleneck with separately constructed database and table // interfaces, especially for processes that run in parallel /** The relational cache of geocoded residential addresses. * * @see ConstructionContext#database() */ public Geocode.Table geocodeTable() { return geocodeTable; } private final Geocode.Table geocodeTable; /** Returns true if this run is restricted to a single client thread, false if * multiple client threads are allowed. */ public boolean isSingleThreaded() { return singleServiceLock != null; } /** Returns all non-poll voter services provided in this run. * * @return newly created array of voter services. * * @see #voterService(String) */ public VoterService[] newVoterServiceArray() { Collection values; synchronized( Run.this ) { values = voterServiceMap.values(); } return values.toArray( new VoterService[values.size()] ); } /** The vote-server for this run. */ public VoteServer voteServer() { return VoteServer.this; } /** API for all polls within the scope of this vote-server run. */ public PollService.VoteServerScope.Run scopePoll() { return scopePoll; } private final PollService.VoteServerScope.Run scopePoll; /** Returns the thread access lock shared by all services; or null if all services * do not share the same lock. They share the same lock only if the run is * {@linkplain #isSingleThreaded() single threaded}. * * @see VoterService#lock() */ public ReentrantLock singleServiceLock() { return singleServiceLock; }; final ReentrantLock singleServiceLock; // when single threaded, there is no need for a lock at all, but having one simpifies the coding of thread-safety assertions /** The trustserver for this vote-server. */ @Warning("thread restricted object") public Trustserver trustserver() { return trustserver; } private final Trustserver trustserver; /** The relational store of service preferences and other settings, * for the users of this vote-server. * * @see ConstructionContext#database() */ @Warning("thread restricted object") public UserSettings.Table userTable() { return userTable; } private final UserSettings.Table userTable; /** Returns a non-poll voter service provided in this run; or null if the name * designates no provided non-poll service. * * @param name per VoterService.{@linkplain VoterService#name() name}(). * * @see VoterService#isNonPoll(String) * @see #init_ensureVoterService(File,Class) * @see #newVoterServiceArray() * @see votorola.a.count.PollService.VoteServerScope.Run#ensurePoll(String) */ public synchronized VoterService voterService( String name ) throws IOException, ScriptException, SQLException { return voterServiceMap.get( name ); } @ThreadRestricted("holds Run.this") private final HashMap voterServiceMap = new HashMap(); } // ==================================================================================== /** A mode of test usage for a vote-server. It allows a particular level of support * for test users to login under unauthenticated aliases. Not all user interfaces * are required to support this; currently only the web interface does. */ public static enum TestUseMode { /** A mode in which test users are disallowed. */ OFF, /** A mode in which test users are allowed and their input is handled exactly as * for authenticated users, except it is subject to arbitrary deletion by the * administrator. */ FULL } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private Database database; // database uninitialized till run commences private static final Logger logger = LoggerX.i( VoteServer.class ); }