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