package votorola.a.web.wic; // Copyright 2008-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.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.regex.*; import javax.script.*; import javax.servlet.http.*; import org.apache.wicket.*; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.resource.loader.*; import org.apache.wicket.request.*; import org.apache.wicket.request.cycle.*; import org.apache.wicket.request.http.*; import org.apache.wicket.settings.*; import org.apache.wicket.util.convert.*; import votorola.a.*; import votorola.a.voter.*; import votorola.a.web.wic.authen.Authenticator; import votorola.g.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.logging.*; import votorola.g.mail.*; import votorola.g.script.*; import votorola.g.util.*; import votorola.g.util.concurrent.ScheduledThreadPoolExecutorX; import votorola.s.wic.WP_Draft; import votorola.s.wic.server.*; /** The Wicket web interface. The home page is {@linkplain WP_Server WP_Server}. * * @see ../../../s/manual.xht#web */ public @ThreadSafe final class VOWicket extends WebApplication { static { try { navBar = new NavBar() { private final ArrayListU tabList = new ArrayListU( new NavTab[] { WP_Server.navBar().superTab().setNavBar( this ), votorola.s.wic.count.WP_CountEngine.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 @Override void init() { initThreadA.set( Thread.currentThread() ); try { // Let admin set configuration // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final ConstructionContext cc; final VoteServer voteServer; final JavaScriptIncluder s; { final String contextPath = getServletContext().getContextPath(); vsRun = new VoteServer( contextPathToVoteServerName( contextPath )) .new Run( /*isSingleThreaded*/false ); vsRun.init_done(); // nothing to do here anymore voteServer = vsRun.voteServer(); startupConfigurationFile = new File( voteServer.votorolaDirectory(), "web/vowicket.js" ); s = new JavaScriptIncluder( startupConfigurationFile ); cc = ConstructionContext.configure( voteServer, contextPath, s ); } ensureWriteable( voteServer.outDirectory() ); // Initialize // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - authenticator = cc.getAuthenticatorClass().getConstructor(VOWicket.class).newInstance( VOWicket.this ); defaultPageIcon = cc.getDefaultPageIcon(); // executor = Executors.newSingleThreadScheduledExecutor( new ThreadFactory() // executor = Executors.unconfigurableScheduledExecutorService( // new ScheduledThreadPoolExecutor( /*pool size*/1, new ThreadFactory() executor = Executors.unconfigurableScheduledExecutorService( new ScheduledThreadPoolExecutorX( /*pool size*/1, new ThreadFactory() { private final AtomicInteger generationCountA = new AtomicInteger(); // informative only public @Override Thread newThread( final Runnable runnable ) { final Thread thread = new Thread( runnable, "web executor " + generationCountA.incrementAndGet() ); executorThreadA.set( thread ); thread.setDaemon( false ); // regardless of current thread's configuration thread.setPriority( Thread.NORM_PRIORITY ); // or as high as group allows return thread; } }, new CatcherL( LoggerX.WARNING ) { public void catchError( Runnable source, Error r ) { log( source, r ); } // log, instead of throwing })); spool.add( new Hold() { public @ThreadSafe void release() { executor.shutdown(); } }); name = cc.getName(); htmlHeaderInsert = cc.getHTMLHeaderInsert(); mirroredContextURI = cc.getMirroredContextURI(); // 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 + '@' + voteServer.serverName(); // Configure markup // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final IMarkupSettings sM = getMarkupSettings(); sM.setDefaultBeforeDisabledLink( "" ); // rather than * sM.setDefaultAfterDisabledLink( "" ); sM.setDefaultMarkupEncoding( "UTF-8" ); sM.setStripWicketTags( true ); // even when getConfigurationType() = RuntimeConfigurationType.DEVELOPMENT. They are stripped regardless for DEPLOYMENT. sM.setStripComments( true ); } // Configure request handling // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setRequestCycleProvider( new IRequestCycleProvider() { public RequestCycle get( final RequestCycleContext c ) { return new VRequestCycle( c ); } }); // final IRequestLoggerSettings sRL = getRequestLoggerSettings(); // sRL.setRequestLoggerEnabled( true ); //// Unreadable, too cluttered. Maybe use Tomcat access logs instead, per Tomcat docs/config/context.html. // if( 1 == 2 ) // TEST, uncomment to temporarily disable stateless checker if( getConfigurationType() == RuntimeConfigurationType.DEVELOPMENT ) { getComponentPostOnBeforeRenderListeners().add( new org.apache.wicket.devutils.stateless.StatelessChecker() ); } // Configure resources. Redirect built-in localizers for validators etc. to // our own bundle, rather than creating a separate bundle for every page. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Cf. java.util.ResourceBundle.Control getResourceSettings().getStringResourceLoaders().add( 0, new IStringResourceLoader() { public @Override String loadStringResource( final Class cl, final String keySuffix, Locale _locale, String _style, String _variation ) { final String className = cl.getName(); final StringBuilder sB = new StringBuilder( className ); if( className.startsWith( BundleFormatter.ASSUMED_PACKAGE_PREFIX )) { sB.delete( 0, BundleFormatter.ASSUMED_PACKAGE_PREFIX.length() ); } sB.append( '.' ); sB.append( keySuffix ); return loadStringResource( sB.toString() ); } public @Override String loadStringResource( final Component component, final String keySuffix, final Locale locale, final String style, final String variation ) { // 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 that it does! Component pageOrComponent = component.getPage(); // we are keying by page class if( pageOrComponent == null ) pageOrComponent = component; return loadStringResource( pageOrComponent.getClass(), keySuffix, locale, style, variation ); } private String loadStringResource( final String key ) { String string = null; try{ string = VRequestCycle.get().bunW().l( key ); } catch( MissingResourceException x ) { LoggerX.i(VOWicket.class).config( // see votorola/a/locale/W.properties "{noTrans, forWic}, key not yet localized: " + key ); } return string; } }); // Mount // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - mount( new PublicConfigRequestMapper( voteServer )); { // Pages // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final HashSet> mountSet = new HashSet>(); mount( navBar, mountSet ); // all the tabbed pages mount( votorola.s.wic.WP_Draft.class, mountSet ); mount( votorola.s.wic.WP_MyDraft.class, mountSet ); mount( votorola.s.wic.diff.WP_D.class, mountSet ); // Redirectors // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` mount( votorola.a.web.wic.redirect.WP_e.class, mountSet ); mount( votorola.a.web.wic.redirect.WP_Diff.class, mountSet ); mount( votorola.a.web.wic.redirect.WP_Pollspace.class, mountSet ); } // Create special scopes // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - scopeActivity = new WP_Activity.ApplicationScope( VOWicket.this ); scopeDraft = new WP_Draft.ApplicationScope( VOWicket.this ); // Let admin initialize // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - s.invokeKnownFunction( "initializingVOWicket", VOWicket.this ); } catch( Throwable x ) { init_throw( x ); } finally { initThreadA.set( null ); } } private static void init_throw( final Throwable x ) { x.printStackTrace( System.err ); // More informative than 'Error filterStart', which is all you get in Tomcat's // catalina.out log. Or for exceptions that precede construction, see the dated // "localhost" logs. They usually have the full trace. if( x instanceof Error ) throw (Error)x; if( x instanceof RuntimeException ) throw (RuntimeException)x; throw new RuntimeException( x ); } // ------------------------------------------------------------------------------------ /** The user authenticator for this web interface. * * @see ConstructionContext#setAuthenticatorClass(Class) */ public Authenticator authenticator() { return authenticator; } private Authenticator authenticator; // final after init /** Returns the name of the vote-server associated with the specified context path. * * @see javax.servlet.ServletContext#getContextPath() */ public static String contextPathToVoteServerName( final String servletContextPath ) { final Matcher m = CONTEXT_PATH_TO_VOTE_SERVER_NAME_PATTERN.matcher( servletContextPath ); if( !m.matches() ) throw new VotorolaRuntimeException( "unknown vote-server name: servlet context path '" + servletContextPath + "' does not match pattern: " + CONTEXT_PATH_TO_VOTE_SERVER_NAME_PATTERN ); return m.group( 1 ); } private static final Pattern CONTEXT_PATH_TO_VOTE_SERVER_NAME_PATTERN = Pattern.compile( "^/([^/]+)(?:/.+)?$" ); /** The default cookie manager for outgoing requests. It accepts cookies from all * domains. It stores them for a single run only. It is strictly for outgoing * requests from this web interface to others; not for incoming requests, which * instead are handled via the servlet API. Use it on a case by case basis. Do not * set it as a {@linkplain CookieHandler#getDefault() global default}. It modifies * the incoming responses by removing their Set-Cookie headers and there is no * efficient, thread-safe way to disable it where it is unwanted. */ public CookieManager cookieManager() { return cookieManager; } private CookieManager cookieManager = new CookieManager( /*store, default*/null, CookiePolicy.ACCEPT_ALL ); /** The location of the default page icon. * * @see ConstructionContext#setDefaultPageIcon(String) * @see ConstructionContext#setDefaultPageIcon(URI) */ public URI defaultPageIcon() { return defaultPageIcon; } private URI defaultPageIcon; // final after init /** The general-purpose web executor, an asynchronous executor that runs on a single * thread, the "web executor" thread. * * @see #isExecutorThread() */ public ScheduledExecutorService executor() { return executor; } private ScheduledExecutorService executor; // final after init private final AtomicReference executorThreadA = new AtomicReference(); /** The site-specific, customized insert for the 'head' section of every HTML page. * The format is strict XHTML. * * @return the insert string, or null if there is none. * * @see ConstructionContext#setHTMLHeaderInsert(String) */ public String htmlHeaderInsert() { return htmlHeaderInsert; } private String htmlHeaderInsert; // final after init /** Answers whether the calling thread is the web executor thread. * * @see #executor() */ public boolean isExecutorThread() { return Thread.currentThread().equals( executorThreadA.get() ); } /** 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 absolute URI of the static mirror of the context directory, or null if the * context directory is not statically served. The URI is specified without a * trailing slash (/). * * @see ConstructionContext#setMirroredContextLocation(String) * @see ConstructionContext#setMirroredContextURI(URI) * @see VRequestCycle#staticContextLocation() */ public URI mirroredContextURI() { return mirroredContextURI; } private URI mirroredContextURI; // 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 VoterService.{@linkplain * VoterService#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 vote-server. */ public static NavBar navBar() { return navBar; } private static NavBar navBar; // final after static init // /** The directory for storage of persistent files that are generated by the web // * interface. The directory is created at runtime if it did not already exist. // * // *

This is the same as the vote-server output directory if that directory is // * writeable by the web interface (servlet container); otherwise, it is some other, // * fallback directory.

// * // * // * @see VoteServer#outDirectory() // */ // public File outDirectory() { return outDirectory; } // // // private File outDirectory; // final after init /** The application scope for instances of WP_Activity. */ public WP_Activity.ApplicationScope scopeActivity() { return scopeActivity; } private WP_Activity.ApplicationScope scopeActivity; // final after init /** The application scope for instances of WP_Draft. */ public WP_Draft.ApplicationScope scopeDraft() { return scopeDraft; } private WP_Draft.ApplicationScope scopeDraft; // final after init /** The email address that nominally identifies the web interface. It is constructed * from the interface and server names, as: * *
{@linkplain #name()
      * INTERFACE-NAME}@{@linkplain VoteServer#serverName()
      * SERVER-NAME}
* *

Email authentication messages to users will be sent from this address. * The mail meta-service ought to respond helpfully to any message that happens to be * sent in reply, to this address.

* * @see ConstructionContext#setName(String) * @see votorola.s.mail.MailMetaService * @see votorola.s.mail.MailMetaService#serviceEmail(VoterService) */ public String serviceEmail() { return serviceEmail; } private String serviceEmail; // final after init /** The spool that is unwound prior to destruction of the web interface. */ public Spool spool() { return spool; } private final Spool spool = new SpoolT(); // need this earlier? you'd be better off refering it from init(), which has an exception handler /** The startup configuration file 'vowicket.js' for this web interface. The language * is JavaScript. There are restrictions on the {@linkplain * votorola.g.script.JavaScriptIncluder character encoding}. * * @see ../../manual.xht#vowicket.js */ File startupConfigurationFile() { return startupConfigurationFile; } private File startupConfigurationFile; // final after init /** The vote-server run for which this web interface is provided. */ public final VoteServer.Run vsRun() { return vsRun; } private VoteServer.Run vsRun; // final after init // - A p p l i c a t i o n ------------------------------------------------------------ /** Returns the intance of VOWicket associated with the current thread. * * @see Session#get() */ public static VOWicket get() { return (VOWicket)WebApplication.get(); } public @Override Class getHomePage() { return WP_Server.class; } protected @Override IConverterLocator newConverterLocator() { final ConverterLocator cL = new ConverterLocator(); // cL.set( java.util.regex.Pattern.class, new votorola.g.util.regex.WicPatternConverter() ); //// till needed again cL.set( IDPair.class, new IDPairConverter() ); return cL; } public @Override Session newSession( final Request request, final Response response ) { return new VSession( (WebRequest)request, (WebResponse)response, VOWicket.this ); } protected @Override void onDestroy() { spool.unwind(); super.onDestroy(); } // ==================================================================================== /** A context for configuring the web interface. The web interface is configured by * its {@linkplain #startupConfigurationFile startup configuration file}, which * contains a script (s) for that purpose. During construction of the web interface, * an instance of this context is passed to s, via s::constructingVOWicket(wicCC). * *

After the interface is running, it itself is passed to s, via * s::initializingVOWicket({@linkplain VOWicket wic}).

*/ public static @ThreadSafe final class ConstructionContext { /** Constructs the complete configuration of the web interface. * * @param s the compiled startup configuration script. */ private static ConstructionContext configure( VoteServer _voteServer, String _contextPath, final JavaScriptIncluder s ) throws ScriptException, URISyntaxException { final ConstructionContext cc = new ConstructionContext( _voteServer, _contextPath, s ); s.invokeKnownFunction( "constructingVOWicket", cc ); return cc; } private ConstructionContext( final VoteServer voteServer, final String contextPath, final JavaScriptIncluder s ) { startupConfigurationFile = s.scriptFile(); mailTransferService = new SMTPTransportX.ConstructionContext( startupConfigurationFile ); name = voteServer.name(); try { defaultPageIcon = new URI( contextPath + "/icon-16.png" ); } catch( URISyntaxException x ) { throw new RuntimeException( x ); } } private final File startupConfigurationFile; // -------------------------------------------------------------------------------- /** The class of user authenticator for the web interface. * * @see VOWicket#authenticator() * @see #setAuthenticatorClass(Class) */ public Class getAuthenticatorClass() { return authenticatorClass; } private Class authenticatorClass = votorola.a.web.wic.authen.OpenIDAuthenticator.class; /** Sets the class of user authenticator for the web interface. Set it like * this, for example:
              *
              *   wicCC.setAuthenticatorClass(
              *     Packages.votorola.a.web.wic.authen.WikiAuthenticator );
* *

The default class is {@linkplain * votorola.a.web.wic.authen.OpenIDAuthenticator OpenIDAuthenticator}.

* * @see VOWicket#authenticator() */ @ThreadRestricted("constructor") public void setAuthenticatorClass( final Class cl ) { authenticatorClass = cl; } /** The location of default page icon, or null if the location is the default. * * @see VOWicket#defaultPageIcon() * @see #setDefaultPageIcon(String) * @see #setDefaultPageIcon(URI) */ public URI getDefaultPageIcon() { return defaultPageIcon; } private URI defaultPageIcon = null; /** Sets the location of default page icon. The default value is "{@linkplain * #name() vote-server}/icon-16.png". * * @see VOWicket#defaultPageIcon() */ @ThreadRestricted("constructor") public void setDefaultPageIcon( final String s ) throws URISyntaxException { setDefaultPageIcon( new URI( s )); } /** Sets the location of default page icon. The default value is "{@linkplain * #name() vote-server}/icon-16.png". * * @see VOWicket#defaultPageIcon() */ @ThreadRestricted("constructor") public void setDefaultPageIcon( final URI uri ) { defaultPageIcon = uri; } /** The site-specific, customized insert for the 'head' section. * * @see VOWicket#htmlHeaderInsert() * @see #setHTMLHeaderInsert(String) */ public String getHTMLHeaderInsert() { return htmlHeaderInsert; } private String htmlHeaderInsert; /** Sets the site-specific, customized insert for the 'head' section. * * @see VOWicket#htmlHeaderInsert() */ @ThreadRestricted("constructor") public void setHTMLHeaderInsert( final String _htmlHeaderInsert ) { htmlHeaderInsert = _htmlHeaderInsert; } /** The absolute URI of the static mirror of the context directory, or null if the * context directory is not statically served. * * @see VOWicket#mirroredContextURI() * @see #setMirroredContextLocation(String) * @see #setMirroredContextURI(URI) */ public URI getMirroredContextURI() { return mirroredContextURI; } private URI mirroredContextURI = null; /** Sets absolute URI of the static mirror of the context directory. The * default value is null which means the context directory is not statically * served. * * @see VOWicket#mirroredContextURI() * @throws IllegalArgumentException if the URI ends with a slash '/' character. */ @ThreadRestricted("constructor") public void setMirroredContextLocation( final String s ) throws URISyntaxException { setMirroredContextURI( new URI( s )); } /** Sets absolute URI of the static mirror of the context directory. The * default value is null which means the context directory is not statically * served. * * @see VOWicket#mirroredContextURI() * @throws IllegalArgumentException if the URI ends with a slash '/' character. */ public @ThreadRestricted("constructor") void setMirroredContextURI( final URI uri ) { if( uri != null && uri.toString().endsWith( "/" )) { throw new IllegalArgumentException( "URI ends with '/'" ); } mirroredContextURI = uri; } /** The name that nominally identifies the web interface and is used to construct * its service email address. * * @see VOWicket#name() * @see VOWicket#serviceEmail() * @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 * VoteServer#name() vote-server name}. * * @see VOWicket#name() * @see VOWicket#serviceEmail() */ @ThreadRestricted("constructor") public void setName( String _name ) { name = _name; } /** The context for configuring access to the mail transfer server, * through which outgoing messages (for email address authentication) are sent. */ public SMTPTransportX.ConstructionContext mailTransferService() { return mailTransferService; } private final SMTPTransportX.ConstructionContext mailTransferService; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private void ensureWriteable( final File dir ) throws IOException { if( !dir.canWrite() ) // writeable by servlet container? { throw new IOException( "web interface (" + System.getProperty("user.name") + ") lacks write permissions for directory: " + dir ); // fail fast } } /** Set at init start, cleared at end. */ private final AtomicReference initThreadA = new AtomicReference(); private void mount( final NavBar bar, final HashSet> mountSet ) { for( NavTab tab: bar.tabList() ) { if( tab instanceof SuperTab ) { mount( ((SuperTab)tab).subBar(), mountSet ); continue; } final Class pageClass = tab.pageClass(); if( pageClass == null ) continue; mount( pageClass, mountSet ); } } private void mount( final Class pageClass, final HashSet> mountSet ) { if( pageClass.equals( WP_Server.class )) return; // home page, already mounted on / if( !mountSet.add(pageClass) ) return; // already mounted final StringBuilder pathB = new StringBuilder( pageClass.getSimpleName() ); if( pathB.indexOf("WP_") == 0 ) pathB.delete( 0, 3 ); // strip leading "WP_" pathB.insert( 0, '/' ); mountPage( pathB.toString(), pageClass ); // creates a MountedMapper } }