package votorola.a.web; // Copyright 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.security.*; import java.util.*; import java.util.logging.*; import votorola.g.logging.*; import java.util.regex.*; import javax.script.*; import org.apache.wicket.*; import org.apache.wicket.protocol.http.*; import org.apache.wicket.resource.loader.*; import org.apache.wicket.settings.*; import org.openid4java.consumer.*; import votorola._.*; import votorola.a.*; import votorola.a.election.district.*; import votorola.a.register.WP_Register; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.mail.*; import votorola.g.script.*; import votorola.g.util.*; /** Primary class of the Web-based voter interface. */ public @ThreadSafe final class VApplication extends WebApplication { static { try { navBar = new NavBar() { private final ArrayListU tabList = new ArrayListU( new NavTab[] { WP_Meta.navBar().superTab().setNavBar( this ), WP_Register.navBar().superTab().setNavBar( this ), WP_Regional.navBar().superTab().setNavBar( this ), }); public SuperTab superTab() { return null; } // null, this is the top bar public List tabList() { return tabList; } }; } catch( Throwable x ) { init_throw( x ); } } protected void init() { try { // Let admin set configuration. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final ConfigurationContext cc; final ElectoralSubserver subserver; final JavaScriptIncluder s; { final Pattern pContextToSubserver = Pattern.compile( "^/([^/]+)(?:/.+)?$" ); final String contextPath = getServletContext().getContextPath(); final Matcher m = pContextToSubserver.matcher( contextPath ); if( !m.matches() ) throw new VotorolaRuntimeException( "unknown subserver name - servlet context path '" + contextPath + "' does not match pattern: " + pContextToSubserver ); subserverRun = new ElectoralSubserver( m.group( 1 )) .new Run( /*isSingleThreaded*/false ); subserver = subserverRun.subserver(); s = new JavaScriptIncluder( new File( subserver.votorolaDirectory(), "web.js" )); cc = ConfigurationContext.configure( subserver, s ); } // Initialize. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - configurationFile = cc.configurationFile; name = cc.getName(); styleSheet = cc.getStyleSheet(); { File dir = subserver.cacheDirectory(); if( !dir.canWrite() ) // writeable by servlet container? { dir = new File( System.getProperty( "java.io.tmpdir" ), subserver.name() ); // fall back - tomcat alters this system property, pointing to its own tmp directory, which is writeable of course if( !dir.isDirectory() && !dir.mkdirs() ) throw new IOException( "unable to create fallback cache directory: " + dir.getPath() ); } cacheDirectory = dir; } consumerManager = new ConsumerManager(); // Configure mail. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - mailSender = new MailSender( cc.mailTransferService() ); { final Properties p = new Properties( System.getProperties() ); { SMTPTransportX.SimpleAuthentication transferAuthentication = cc.mailTransferService().getAuthenticationMethod(); if( transferAuthentication != null ) p.put( "mail.smtp.auth", "true" ); } mailSession = javax.mail.Session.getInstance( p ); } serviceEmail = name + '@' + subserver.domainName(); // Configure markup. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final IMarkupSettings sM = getMarkupSettings(); sM.setDefaultBeforeDisabledLink( "" ); // rather than * sM.setDefaultAfterDisabledLink( "" ); sM.setDefaultMarkupEncoding( "UTF-8" ); sM.setStripWicketTags( true ); // even when getConfigurationType().equals(DEVELOPMENT) - they are stripped regardless for DEPLOYMENT sM.setStripComments( true ); } // Configure request logging. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { // final IRequestLoggerSettings sRL = getRequestLoggerSettings(); // sRL.setRequestLoggerEnabled( true ); //// Unreadable, too cluttered. Maybe use Tomcat access logs instead, per Tomcat docs/config/context.html. } // Configure resources. Redirect built-in localizers (for validators etc.) // to our own bundle; rather than creating a separate bundle for every page. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final IResourceSettings sR = getResourceSettings(); final Object[] originalStringLoaderArray = // before clobbering them, as adding a custom one (below) will do sR.getStringResourceLoaders().toArray(); sR.addStringResourceLoader( new IStringResourceLoader() { private static final String packagePrefix = "votorola."; public @Override String loadStringResource( Component component, String keySuffix ) { // if( component == null ) // { // assert false; // only seen when doing weird things, like adding validation errors from outside of validation process // return loadStringResource( keySuffix ); // } //// no, this means that the component has no parent, yet - ensure it does! Component pageOrComponent = component.getPage(); // we are keying by page class if( pageOrComponent == null ) pageOrComponent = component; return loadStringResource( pageOrComponent.getClass(), keySuffix, component.getLocale(), component.getStyle() ); } public @Override String loadStringResource( Class cl, String keySuffix, Locale locale, String style ) { final String className = cl.getName(); final StringBuilder sB = new StringBuilder( className ); if( className.startsWith( packagePrefix )) { sB.delete( 0, packagePrefix.length() ); } sB.append( '.' ); sB.append( keySuffix ); return loadStringResource( sB.toString() ); } private String loadStringResource( final String key ) { String string = null; try { string = VRequestCycle.get().bunW().l( key ); } catch( MissingResourceException x ) { logger.config( "built-in Wicket message, key not yet localized: " + key ); } // see values 'XXX' in votorola/a/locale/W.properties return string; } }); for( Object o: originalStringLoaderArray ) // add them back { sR.addStringResourceLoader( (IStringResourceLoader)o ); } } // Start electoral services. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - subserverRun.init_ensureAllElectoralServices(); subserverRun.init_done(); // Create special scopes. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - scopeHelp = new WP_Help.ApplicationScope(); scopeMeta = new WP_Meta.ApplicationScope( VApplication.this ); // Let admin initialize. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - s.invokeKnownFunction( "initWeb", VApplication.this ); } catch( Throwable x ) { init_throw( x ); } } private static void init_throw( final Throwable x ) { x.printStackTrace( System.err ); // more informative than 'Error filterStart', which is all you get in the Tomcat logs if( x instanceof Error ) throw (Error)x; if( x instanceof RuntimeException ) throw (RuntimeException)x; throw new RuntimeException( x ); } // ------------------------------------------------------------------------------------ /** Directory for storage of Web interface files that are generated at runtime, * and persisted from run to run. The directory is created at runtime * if it did not already exist. *

* This is the same as the subserver cache directory, * if that directory is writeable by the electoral services daemon * of the Web interface (i.e. the servlet container); * otherwise, it is some other, fallback directory. *

* * @see ElectoralSubserver#cacheDirectory() */ public File cacheDirectory() { return cacheDirectory; } private File cacheDirectory; // final after init /** The scripted configuration file for this Web interface. It is located at: *
* {@linkplain ElectoralSubserver#votorolaDirectory() votorolaDirectory}/web.js *
*

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

*/ File configurationFile() { return configurationFile; } private File configurationFile; // final after init /** The OpenID consumer manager. */ public @ThreadRestricted("holds openIDLock") ConsumerManager consumerManager() { assert Thread.holdsLock( openIDLock ); // actually, it's the object that is restricted, not this method return consumerManager; } private ConsumerManager consumerManager; // final after init /** Lock object for mail facilities. This object's monitor lock synchronizes all * access to members that are annotated * @{@linkplain ThreadRestricted ThreadRestricted}("holds mailLock"). */ public Object mailLock() { return mailLock; } private final Object mailLock = new Object(); /** Access to the SMTP mail transfer service. */ public @ThreadRestricted("holds mailLock") MailSender mailSender() { assert Thread.holdsLock( mailLock ); // actually, it's the object that is restricted, not this method return mailSender; } private MailSender mailSender; // final after init /** The mail session for this run of the Web interface. */ public @ThreadRestricted("holds mailLock") javax.mail.Session mailSession() { assert Thread.holdsLock( mailLock ); // actually, it's the object that is restricted, not this method return mailSession; } private javax.mail.Session mailSession; // final after init /** The name that nominally identifies this Web interface. * It must be valid as the local part (before the '@') of an email address, * per ElectoralService.{@linkplain ElectoralService#name() name}(). * * @see #serviceEmail() */ public String name() { return name; } private String name; // final after init /** The top navigation bar, for navigating among the pages of the subserver. */ public static NavBar navBar() { return navBar; } private static NavBar navBar; // final after static init /** Lock object for openID facilities. This object's monitor lock synchronizes all * access to members that are annotated * @{@linkplain ThreadRestricted ThreadRestricted}("holds openIDLock"). */ public Object openIDLock() { return openIDLock; } private final Object openIDLock = new Object(); /** Returns the application scope for instances of WP_Help. */ public WP_Help.ApplicationScope scopeHelp() { return scopeHelp; } private WP_Help.ApplicationScope scopeHelp; // final after init /** Returns the application scope for instances of WP_Meta. */ public WP_Meta.ApplicationScope scopeMeta() { return scopeMeta; } private WP_Meta.ApplicationScope scopeMeta; // final after init /** The secure random number generator. */ public @ThreadRestricted("holds mailLock") SecureRandom secureRandomizer() { assert Thread.holdsLock( mailLock ); // actually, it's the object that is restricted, not this method return secureRandomizer; } private final SecureRandom secureRandomizer = new SecureRandom(); /** The email address that nominally identifies the Web interface. It is constructed * from the interface name and subserver domain name, * as '{@linkplain #name() name}@{@linkplain ElectoralSubserver#domainName() domainName}'. * Email authentication messages to users will be sent from this address. * Someone or something (such as * the {@linkplain votorola.a.mail.MailMetaService mail meta-service}) * ought to respond helpfully to any message that happens to be sent to this address. * * @see ConfigurationContext#setName(String) */ public String serviceEmail() { return serviceEmail; } private String serviceEmail; // final after init /** Spool unwound prior to destruction of this application. */ Spool spool() { return spool; } private final Spool spool = new SpoolT(); /** The URL of the subserver's style sheet; or null, if there is none. * Its purpose is to customize the subserver's page styling. */ public String styleSheet() { return styleSheet; } private String styleSheet; // final after init /** The subserver run, for which this Web responder is provided. */ public final ElectoralSubserver.Run subserverRun() { return subserverRun; } private ElectoralSubserver.Run subserverRun; // final after init /** The email address of the test user to automatically login, * according to system property "votorola.testUserEmail". May be null. */ public static final String TEST_USER_EMAIL = System.getProperty( "votorola.testUserEmail" ); // - A p p l i c a t i o n ------------------------------------------------------------ public @Override Class getHomePage() { return WP_Meta.class; } public @Override Session newSession( Request request, Response response ) { return new VSession( request, VApplication.this ); } public @Override RequestCycle newRequestCycle( Request request, Response response ) { return new VRequestCycle( VApplication.this, (WebRequest)request, response ); } // ==================================================================================== /** A context [empty, not yet used] for configuring the Web interface. * The Web interface is configured by its * {@linkplain #configurationFile configuration file}, * which contains a script (s) for that purpose. * During construction of the Web interface, an instance of this context (webCC) * is passed to s, via s::configureWeb(webCC). */ public static @ThreadSafe final class ConfigurationContext // public class and getters, accessible by configuration scripts { /** Constructs the complete configuration of the Web interface. * * @param s the compiled configuration script */ private static ConfigurationContext configure( ElectoralSubserver subserver, JavaScriptIncluder s ) throws ScriptException { final ConfigurationContext cc = new ConfigurationContext( subserver, s ); s.invokeKnownFunction( "configureWeb", cc ); return cc; } private ConfigurationContext( ElectoralSubserver subserver, JavaScriptIncluder s ) { configurationFile = s.scriptFile(); mailTransferService = new SMTPTransportX.ConfigurationContext( configurationFile ); name = subserver.name(); } private final File configurationFile; // -------------------------------------------------------------------------------- /** @see #setName(String) */ public String getName() { return name; } private String name; /** Sets the name that nominally identifies the Web interface, and is used to * construct its service email address. The default value is the {@linkplain * ElectoralSubserver#name() subserver name}. * * @see VApplication#name() * @see VApplication#serviceEmail() * @see #getName() */ @ThreadRestricted("constructor") public void setName( String name ) { this.name = name; } /** @see #setStyleSheet(String) */ public String getStyleSheet() { return styleSheet; } private String styleSheet; /** Sets the URL of the subserver's custom style sheet. The default value is null. * * @see VApplication#styleSheet() * @see #getStyleSheet() */ @ThreadRestricted("constructor") public void setStyleSheet( String styleSheet ) { this.styleSheet = styleSheet; } /** The context for configuring access to the mail transfer server, * through which outgoing messages (for email address authentication) are sent. */ public SMTPTransportX.ConfigurationContext mailTransferService() { return mailTransferService; } private final SMTPTransportX.ConfigurationContext mailTransferService; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Logger logger = LoggerX.i( VApplication.class ); // - A p p l i c a t i o n ------------------------------------------------------------ protected @Override void onDestroy() { spool.unwind(); super.onDestroy(); } }