001package 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.
002
003import java.io.*;
004import java.net.*;
005import java.util.*;
006import java.util.concurrent.*;
007import java.util.concurrent.atomic.*;
008import java.util.regex.*;
009import javax.script.*;
010import javax.servlet.http.*;
011import org.apache.wicket.*;
012import org.apache.wicket.protocol.http.WebApplication;
013import org.apache.wicket.resource.loader.*;
014import org.apache.wicket.request.*;
015import org.apache.wicket.request.cycle.*;
016import org.apache.wicket.request.http.*;
017import org.apache.wicket.settings.*;
018import org.apache.wicket.util.convert.*;
019import votorola.a.*;
020import votorola.a.voter.*;
021import votorola.a.web.wic.authen.Authenticator;
022import votorola.g.*;
023import votorola.g.hold.*;
024import votorola.g.lang.*;
025import votorola.g.locale.*;
026import votorola.g.logging.*;
027import votorola.g.mail.*;
028import votorola.g.script.*;
029import votorola.g.util.*;
030import votorola.g.util.concurrent.ScheduledThreadPoolExecutorX;
031import votorola.s.wic.WP_Draft;
032import votorola.s.wic.server.*;
033
034
035/** The Wicket web interface.  The home page is {@linkplain WP_Server WP_Server}.
036  *
037  *     @see <a href='../../../../../../s/manual.xht#web' target='_top'
038  *                           >../../../s/manual.xht#web</a>
039  */
040public @ThreadSafe final class VOWicket extends WebApplication
041{
042
043    static
044    {
045        try
046        {
047            navBar = new NavBar()
048            {
049                private final ArrayListU<NavTab> tabList = new ArrayListU<NavTab>( new NavTab[]
050                {
051                    WP_Server.navBar().superTab().setNavBar( this ),
052                    votorola.s.wic.count.WP_CountEngine.navBar().superTab().setNavBar( this ),
053                });
054
055                public SuperTab superTab() { return null; } // null, this is the top bar
056
057                public List<NavTab> tabList() { return tabList; }
058
059            };
060        }
061        catch( Throwable x ) { init_throw( x ); }
062    }
063
064
065
066    protected @Override void init()
067    {
068        initThreadA.set( Thread.currentThread() );
069        try
070        {
071
072          // Let admin set configuration
073          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
074            final ConstructionContext cc;
075            final VoteServer voteServer;
076            final JavaScriptIncluder s;
077            {
078                final String contextPath = getServletContext().getContextPath();
079                vsRun = new VoteServer( contextPathToVoteServerName( contextPath ))
080                  .new Run( /*isSingleThreaded*/false );
081                vsRun.init_done(); // nothing to do here anymore
082
083                voteServer = vsRun.voteServer();
084                startupConfigurationFile = new File( voteServer.votorolaDirectory(),
085                  "web/vowicket.js" );
086
087                s = new JavaScriptIncluder( startupConfigurationFile );
088                cc = ConstructionContext.configure( voteServer, contextPath, s );
089            }
090            ensureWriteable( voteServer.outDirectory() );
091
092          // Initialize
093          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
094            authenticator = cc.getAuthenticatorClass().getConstructor(VOWicket.class).newInstance(
095              VOWicket.this );
096            defaultPageIcon = cc.getDefaultPageIcon();
097
098        // executor = Executors.newSingleThreadScheduledExecutor( new ThreadFactory()
099        // executor = Executors.unconfigurableScheduledExecutorService(
100        //   new ScheduledThreadPoolExecutor( /*pool size*/1, new ThreadFactory()
101           executor = Executors.unconfigurableScheduledExecutorService(
102             new ScheduledThreadPoolExecutorX( /*pool size*/1, new ThreadFactory()
103            {
104                private final AtomicInteger generationCountA = new AtomicInteger(); // informative only
105                public @Override Thread newThread( final Runnable runnable )
106                {
107                    final Thread thread = new Thread( runnable, "web executor "
108                      + generationCountA.incrementAndGet() );
109                    executorThreadA.set( thread );
110                    thread.setDaemon( false ); // regardless of current thread's configuration
111                    thread.setPriority( Thread.NORM_PRIORITY ); // or as high as group allows
112                    return thread;
113                }
114            }, new CatcherL<Runnable>( LoggerX.WARNING )
115            {
116                public void catchError( Runnable source, Error r ) { log( source, r ); } // log, instead of throwing
117            }));
118            spool.add( new Hold()
119            {
120                public @ThreadSafe void release() { executor.shutdown(); }
121            });
122
123            name = cc.getName();
124            htmlHeaderInsert = cc.getHTMLHeaderInsert();
125            mirroredContextURI = cc.getMirroredContextURI();
126
127          // Configure mail
128          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
129            mailSender = new MailSender( cc.mailTransferService() );
130            {
131                final Properties p = new Properties( System.getProperties() );
132                {
133                    SMTPTransportX.SimpleAuthentication transferAuthentication =
134                      cc.mailTransferService().getAuthenticationMethod();
135                    if( transferAuthentication != null ) p.put( "mail.smtp.auth", "true" );
136                }
137                mailSession = javax.mail.Session.getInstance( p );
138            }
139            serviceEmail = name + '@' + voteServer.serverName();
140
141          // Configure markup
142          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
143            {
144                final IMarkupSettings sM = getMarkupSettings();
145
146                sM.setDefaultBeforeDisabledLink( "<span class='disabled-link'>" ); // rather than <em>*</em>
147                sM.setDefaultAfterDisabledLink( "</span>" );
148
149                sM.setDefaultMarkupEncoding( "UTF-8" );
150                sM.setStripWicketTags( true ); // even when getConfigurationType() = RuntimeConfigurationType.DEVELOPMENT.  They are stripped regardless for DEPLOYMENT.
151                sM.setStripComments( true );
152            }
153
154          // Configure request handling
155          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
156            setRequestCycleProvider( new IRequestCycleProvider()
157            {
158                public RequestCycle get( final RequestCycleContext c )
159                {
160                    return new VRequestCycle( c );
161                }
162            });
163         // final IRequestLoggerSettings sRL = getRequestLoggerSettings();
164         // sRL.setRequestLoggerEnabled( true );
165         //// Unreadable, too cluttered.  Maybe use Tomcat access logs instead, per Tomcat docs/config/context.html.
166
167         // if( 1 == 2 ) // TEST, uncomment to temporarily disable stateless checker
168            if( getConfigurationType() == RuntimeConfigurationType.DEVELOPMENT )
169            {
170                getComponentPostOnBeforeRenderListeners().add(
171                  new org.apache.wicket.devutils.stateless.StatelessChecker() );
172            }
173
174          // Configure resources.  Redirect built-in localizers for validators etc.  to
175          // our own bundle, rather than creating a separate bundle for every page.
176          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
177          // Cf. java.util.ResourceBundle.Control
178            getResourceSettings().getStringResourceLoaders().add( 0, new IStringResourceLoader()
179            {
180                public @Override String loadStringResource( final Class<?> cl,
181                  final String keySuffix, Locale _locale, String _style, String _variation )
182                {
183                    final String className = cl.getName();
184                    final StringBuilder sB = new StringBuilder( className );
185                    if( className.startsWith( BundleFormatter.ASSUMED_PACKAGE_PREFIX ))
186                    {
187                        sB.delete( 0, BundleFormatter.ASSUMED_PACKAGE_PREFIX.length() );
188                    }
189
190                    sB.append( '.' );
191                    sB.append( keySuffix );
192                    return loadStringResource( sB.toString() );
193                }
194
195                public @Override String loadStringResource( final Component component,
196                  final String keySuffix, final Locale locale, final String style,
197                  final String variation )
198                {
199                 // if( component == null )
200                 // {
201                 //     assert false; // only seen when doing weird things, like adding validation errors from outside of validation process
202                 //     return loadStringResource( keySuffix );
203                 // }
204                 //// no, this means that the component has no parent yet; ensure that it does!
205
206                    Component pageOrComponent = component.getPage(); // we are keying by page class
207                    if( pageOrComponent == null ) pageOrComponent = component;
208
209                    return loadStringResource( pageOrComponent.getClass(), keySuffix, locale, style,
210                      variation );
211                }
212
213                private String loadStringResource( final String key )
214                {
215                    String string = null;
216                    try{ string = VRequestCycle.get().bunW().l( key ); }
217                    catch( MissingResourceException x )
218                    {
219                        LoggerX.i(VOWicket.class).config( // see votorola/a/locale/W.properties
220                          "{noTrans, forWic}, key not yet localized: " + key );
221                    }
222                    return string;
223                }
224            });
225
226          // Mount
227          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
228            mount( new PublicConfigRequestMapper( voteServer ));
229            {
230              // Pages
231              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
232                final HashSet<Class<? extends Page>> mountSet = new HashSet<Class<? extends Page>>();
233                mount( navBar, mountSet ); // all the tabbed pages
234                mount( votorola.s.wic.WP_Draft.class, mountSet );
235                mount( votorola.s.wic.WP_MyDraft.class, mountSet );
236                mount( votorola.s.wic.diff.WP_D.class, mountSet );
237
238              // Redirectors
239              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
240                mount( votorola.a.web.wic.redirect.WP_e.class, mountSet );
241                mount( votorola.a.web.wic.redirect.WP_Diff.class, mountSet );
242                mount( votorola.a.web.wic.redirect.WP_Pollspace.class, mountSet );
243            }
244
245          // Create special scopes
246          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
247            scopeActivity = new WP_Activity.ApplicationScope( VOWicket.this );
248            scopeDraft = new WP_Draft.ApplicationScope( VOWicket.this );
249
250          // Let admin initialize
251          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
252            s.invokeKnownFunction( "initializingVOWicket", VOWicket.this );
253        }
254        catch( Throwable x ) { init_throw( x ); }
255        finally
256        {
257            initThreadA.set( null );
258        }
259    }
260
261
262
263    private static void init_throw( final Throwable x )
264    {
265        x.printStackTrace( System.err );
266          // More informative than 'Error filterStart', which is all you get in Tomcat's
267          // catalina.out log.  Or for exceptions that precede construction, see the dated
268          // "localhost" logs.  They usually have the full trace.
269        if( x instanceof Error ) throw (Error)x;
270        if( x instanceof RuntimeException ) throw (RuntimeException)x;
271        throw new RuntimeException( x );
272    }
273
274
275
276   // ------------------------------------------------------------------------------------
277
278
279    /** The user authenticator for this web interface.
280      *
281      *     @see ConstructionContext#setAuthenticatorClass(Class)
282      */
283    public Authenticator authenticator() { return authenticator; }
284
285
286        private Authenticator authenticator; // final after init
287
288
289
290    /** Returns the name of the vote-server associated with the specified context path.
291      *
292      *     @see javax.servlet.ServletContext#getContextPath()
293      */
294    public static String contextPathToVoteServerName( final String servletContextPath )
295    {
296        final Matcher m = CONTEXT_PATH_TO_VOTE_SERVER_NAME_PATTERN.matcher( servletContextPath );
297        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 );
298
299        return m.group( 1 );
300    }
301
302
303        private static final Pattern CONTEXT_PATH_TO_VOTE_SERVER_NAME_PATTERN
304          = Pattern.compile( "^/([^/]+)(?:/.+)?$" );
305
306
307
308    /** The default cookie manager for outgoing requests.  It accepts cookies from all
309      * domains.  It stores them for a single run only.  It is strictly for outgoing
310      * requests from this web interface to others; not for incoming requests, which
311      * instead are handled via the servlet API.  Use it on a case by case basis.  Do not
312      * set it as a {@linkplain CookieHandler#getDefault() global default}.  It modifies
313      * the incoming responses by removing their Set-Cookie headers and there is no
314      * efficient, thread-safe way to disable it where it is unwanted.
315      */
316    public CookieManager cookieManager() { return cookieManager; }
317
318
319        private CookieManager cookieManager = new CookieManager( /*store, default*/null,
320          CookiePolicy.ACCEPT_ALL );
321
322
323
324    /** The location of the default page icon.
325      *
326      *     @see ConstructionContext#setDefaultPageIcon(String)
327      *     @see ConstructionContext#setDefaultPageIcon(URI)
328      */
329    public URI defaultPageIcon() { return defaultPageIcon; }
330
331
332        private URI defaultPageIcon; // final after init
333
334
335
336    /** The general-purpose web executor, an asynchronous executor that runs on a single
337      * thread, the "web executor" thread.
338      *
339      *     @see #isExecutorThread()
340      */
341    public ScheduledExecutorService executor() { return executor; }
342
343
344        private ScheduledExecutorService executor; // final after init
345
346
347        private final AtomicReference<Thread> executorThreadA = new AtomicReference<Thread>();
348
349
350
351    /** The site-specific, customized insert for the 'head' section of every HTML page.
352      * The format is strict XHTML.
353      *
354      *     @return the insert string, or null if there is none.
355      *
356      *     @see ConstructionContext#setHTMLHeaderInsert(String)
357      */
358    public String htmlHeaderInsert() { return htmlHeaderInsert; }
359
360
361        private String htmlHeaderInsert; // final after init
362
363
364
365    /** Answers whether the calling thread is the web executor thread.
366      *
367      *     @see #executor()
368      */
369    public boolean isExecutorThread()
370    {
371        return Thread.currentThread().equals( executorThreadA.get() );
372    }
373
374
375
376    /** Lock object for mail facilities.  This object's monitor lock synchronizes all
377      * access to members that are annotated
378      * <code>@{@linkplain ThreadRestricted ThreadRestricted}("holds mailLock")</code>.
379      */
380    public Object mailLock() { return mailLock; }
381
382
383        private final Object mailLock = new Object();
384
385
386
387    /** Access to the SMTP mail transfer service.
388      */
389    public @ThreadRestricted("holds mailLock") MailSender mailSender()
390    {
391        assert Thread.holdsLock( mailLock ); // actually it's the object that is restricted, not this method
392        return mailSender;
393    }
394
395        private MailSender mailSender; // final after init
396
397
398
399    /** The mail session for this run of the web interface.
400      */
401    public @ThreadRestricted("holds mailLock") javax.mail.Session mailSession()
402    {
403        assert Thread.holdsLock( mailLock ); // actually it's the object that is restricted, not this method
404        return mailSession;
405    }
406
407
408        private javax.mail.Session mailSession; // final after init
409
410
411
412    /** The absolute URI of the static mirror of the context directory, or null if the
413      * context directory is not statically served.  The URI is specified without a
414      * trailing slash (/).
415      *
416      *     @see ConstructionContext#setMirroredContextLocation(String)
417      *     @see ConstructionContext#setMirroredContextURI(URI)
418      *     @see VRequestCycle#staticContextLocation()
419      */
420    public URI mirroredContextURI() { return mirroredContextURI; }
421
422
423        private URI mirroredContextURI; // final after init
424
425
426
427    /** The name that nominally identifies this web interface.  It must be valid as the
428      * local part (before the '@') of an email address, per VoterService.{@linkplain
429      * VoterService#name() name}().
430      *
431      *     @see #serviceEmail()
432      */
433    public String name() { return name; }
434
435
436        private String name; // final after init
437
438
439
440    /** The top navigation bar for navigating among the pages of the vote-server.
441      */
442    public static NavBar navBar() { return navBar; }
443
444
445        private static NavBar navBar; // final after static init
446
447
448
449 // /** The directory for storage of persistent files that are generated by the web
450 //   * interface.  The directory is created at runtime if it did not already exist.
451 //   *
452 //   * <p>This is the same as the vote-server output directory if that directory is
453 //   * writeable by the web interface (servlet container); otherwise, it is some other,
454 //   * fallback directory.</p>
455 //   *
456 //   *
457 //   *     @see VoteServer#outDirectory()
458 //   */
459 // public File outDirectory() { return outDirectory; }
460 //
461 //
462 //     private File outDirectory; // final after init
463
464
465
466    /** The application scope for instances of WP_Activity.
467      */
468    public WP_Activity.ApplicationScope scopeActivity() { return scopeActivity; }
469
470
471        private WP_Activity.ApplicationScope scopeActivity; // final after init
472
473
474
475    /** The application scope for instances of WP_Draft.
476      */
477    public WP_Draft.ApplicationScope scopeDraft() { return scopeDraft; }
478
479
480        private WP_Draft.ApplicationScope scopeDraft; // final after init
481
482
483
484    /** The email address that nominally identifies the web interface.  It is constructed
485      * from the interface and server names, as:
486      *
487      * <pre class='indent vspace'><var>{@linkplain #name()
488      * INTERFACE-NAME}</var>@<var>{@linkplain VoteServer#serverName()
489      * SERVER-NAME}</var></pre>
490      *
491      * <p>Email authentication messages to users will be sent <em>from</em> this address.
492      * The mail meta-service ought to respond helpfully to any message that happens to be
493      * sent in reply, <var>to</var> this address.</p>
494      *
495      *     @see ConstructionContext#setName(String)
496      *     @see votorola.s.mail.MailMetaService
497      *     @see votorola.s.mail.MailMetaService#serviceEmail(VoterService)
498      */
499    public String serviceEmail() { return serviceEmail; }
500
501
502        private String serviceEmail; // final after init
503
504
505
506    /** The spool that is unwound prior to destruction of the web interface.
507      */
508    public Spool spool() { return spool; }
509
510
511        private final Spool spool = new SpoolT(); // need this earlier?  you'd be better off refering it from init(), which has an exception handler
512
513
514
515    /** The startup configuration file 'vowicket.js' for this web interface.  The language
516      * is JavaScript.  There are restrictions on the {@linkplain
517      * votorola.g.script.JavaScriptIncluder character encoding}.
518      *
519      *     @see <a href='../../../../../../s/manual.xht#vowicket.js'
520      *                                >../../manual.xht#vowicket.js</a>
521      */
522    File startupConfigurationFile() { return startupConfigurationFile; }
523
524
525        private File startupConfigurationFile; // final after init
526
527
528
529    /** The vote-server run for which this web interface is provided.
530      */
531    public final VoteServer.Run vsRun() { return vsRun; }
532
533
534        private VoteServer.Run vsRun; // final after init
535
536
537
538   // - A p p l i c a t i o n ------------------------------------------------------------
539
540
541    /** Returns the intance of VOWicket associated with the current thread.
542      *
543      *   @see Session#get()
544      */
545    public static VOWicket get() { return (VOWicket)WebApplication.get(); }
546
547
548
549    public @Override Class<? extends Page> getHomePage() { return WP_Server.class; }
550
551
552
553    protected @Override IConverterLocator newConverterLocator()
554    {
555        final ConverterLocator cL = new ConverterLocator();
556     // cL.set( java.util.regex.Pattern.class, new votorola.g.util.regex.WicPatternConverter() );
557     //// till needed again
558        cL.set( IDPair.class, new IDPairConverter() );
559        return cL;
560    }
561
562
563
564    public @Override Session newSession( final Request request, final Response response )
565    {
566        return new VSession( (WebRequest)request, (WebResponse)response, VOWicket.this );
567    }
568
569
570
571    protected @Override void onDestroy()
572    {
573        spool.unwind();
574        super.onDestroy();
575    }
576
577
578
579   // ====================================================================================
580
581
582    /** A context for configuring the web interface.  The web interface is configured by
583      * its {@linkplain #startupConfigurationFile startup configuration file}, which
584      * contains a script (s) for that purpose.  During construction of the web interface,
585      * an instance of this context is passed to s, via s::constructingVOWicket(wicCC).
586      *
587      * <p>After the interface is running, it itself is passed to s, via
588      * s::initializingVOWicket({@linkplain VOWicket wic}).</p>
589      */
590    public static @ThreadSafe final class ConstructionContext
591    {
592
593
594        /** Constructs the complete configuration of the web interface.
595          *
596          *     @param s the compiled startup configuration script.
597          */
598        private static ConstructionContext configure( VoteServer _voteServer, String _contextPath,
599          final JavaScriptIncluder s ) throws ScriptException, URISyntaxException
600        {
601            final ConstructionContext cc = new ConstructionContext( _voteServer, _contextPath, s );
602            s.invokeKnownFunction( "constructingVOWicket", cc );
603            return cc;
604        }
605
606
607
608        private ConstructionContext( final VoteServer voteServer, final String contextPath,
609          final JavaScriptIncluder s )
610        {
611            startupConfigurationFile = s.scriptFile();
612            mailTransferService = new SMTPTransportX.ConstructionContext( startupConfigurationFile );
613            name = voteServer.name();
614            try
615            {
616                defaultPageIcon = new URI( contextPath + "/icon-16.png" );
617            }
618            catch( URISyntaxException x ) { throw new RuntimeException( x ); }
619        }
620
621
622
623        private final File startupConfigurationFile;
624
625
626
627       // --------------------------------------------------------------------------------
628
629
630        /** The class of user authenticator for the web interface.
631          *
632          *     @see VOWicket#authenticator()
633          *     @see #setAuthenticatorClass(Class)
634          */
635        public Class<? extends Authenticator> getAuthenticatorClass() { return authenticatorClass; }
636
637
638            private Class<? extends Authenticator> authenticatorClass =
639              votorola.a.web.wic.authen.OpenIDAuthenticator.class;
640
641
642            /** Sets the class of user authenticator for the web interface.  Set it like
643              * this, for example:<pre>
644              *
645              *   wicCC.setAuthenticatorClass(
646              *     Packages.votorola.a.web.wic.authen.WikiAuthenticator );</pre>
647              *
648              * <p>The default class is {@linkplain
649              * votorola.a.web.wic.authen.OpenIDAuthenticator OpenIDAuthenticator}.</p>
650              *
651              *     @see VOWicket#authenticator()
652              */
653              @ThreadRestricted("constructor")
654            public void setAuthenticatorClass( final Class<? extends Authenticator> cl )
655            {
656                authenticatorClass = cl;
657            }
658
659
660
661        /** The location of default page icon, or null if the location is the default.
662          *
663          *     @see VOWicket#defaultPageIcon()
664          *     @see #setDefaultPageIcon(String)
665          *     @see #setDefaultPageIcon(URI)
666          */
667        public URI getDefaultPageIcon() { return defaultPageIcon; }
668
669
670            private URI defaultPageIcon = null;
671
672
673            /** Sets the location of default page icon.  The default value is "{@linkplain
674              * #name() vote-server}/icon-16.png".
675              *
676              *     @see VOWicket#defaultPageIcon()
677              */
678              @ThreadRestricted("constructor")
679            public void setDefaultPageIcon( final String s ) throws URISyntaxException
680            {
681                setDefaultPageIcon( new URI( s ));
682            }
683
684
685            /** Sets the location of default page icon.  The default value is "{@linkplain
686              * #name() vote-server}/icon-16.png".
687              *
688              *     @see VOWicket#defaultPageIcon()
689              */
690              @ThreadRestricted("constructor")
691            public void setDefaultPageIcon( final URI uri ) { defaultPageIcon = uri; }
692
693
694
695        /** The site-specific, customized insert for the 'head' section.
696          *
697          *     @see VOWicket#htmlHeaderInsert()
698          *     @see #setHTMLHeaderInsert(String)
699          */
700        public String getHTMLHeaderInsert() { return htmlHeaderInsert; }
701
702
703            private String htmlHeaderInsert;
704
705
706            /** Sets the site-specific, customized insert for the 'head' section.
707              *
708              *     @see VOWicket#htmlHeaderInsert()
709              */
710              @ThreadRestricted("constructor")
711            public void setHTMLHeaderInsert( final String _htmlHeaderInsert )
712            {
713                htmlHeaderInsert = _htmlHeaderInsert;
714            }
715
716
717
718        /** The absolute URI of the static mirror of the context directory, or null if the
719          * context directory is not statically served.
720          *
721          *     @see VOWicket#mirroredContextURI()
722          *     @see #setMirroredContextLocation(String)
723          *     @see #setMirroredContextURI(URI)
724          */
725        public URI getMirroredContextURI() { return mirroredContextURI; }
726
727
728            private URI mirroredContextURI = null;
729
730
731            /** Sets absolute URI of the static mirror of the context directory.  The
732              * default value is null which means the context directory is not statically
733              * served.
734              *
735              *     @see VOWicket#mirroredContextURI()
736              *     @throws IllegalArgumentException if the URI ends with a slash '/' character.
737              */
738              @ThreadRestricted("constructor")
739            public void setMirroredContextLocation( final String s ) throws URISyntaxException
740            {
741                setMirroredContextURI( new URI( s ));
742            }
743
744
745            /** Sets absolute URI of the static mirror of the context directory.  The
746              * default value is null which means the context directory is not statically
747              * served.
748              *
749              *     @see VOWicket#mirroredContextURI()
750              *     @throws IllegalArgumentException if the URI ends with a slash '/' character.
751              */
752            public @ThreadRestricted("constructor") void setMirroredContextURI( final URI uri )
753            {
754                if( uri != null && uri.toString().endsWith( "/" ))
755                {
756                    throw new IllegalArgumentException( "URI ends with '/'" );
757                }
758
759                mirroredContextURI = uri;
760            }
761
762
763
764        /** The name that nominally identifies the web interface and is used to construct
765          * its service email address.
766          *
767          *     @see VOWicket#name()
768          *     @see VOWicket#serviceEmail()
769          *     @see #setName(String)
770          */
771        public String getName() { return name; }
772
773
774            private String name;
775
776
777            /** Sets the name that nominally identifies the web interface, and is used to
778              * construct its service email address.  The default value is the {@linkplain
779              * VoteServer#name() vote-server name}.
780              *
781              *     @see VOWicket#name()
782              *     @see VOWicket#serviceEmail()
783              */
784              @ThreadRestricted("constructor")
785            public void setName( String _name ) { name = _name; }
786
787
788
789        /** The context for configuring access to the mail transfer server,
790          * through which outgoing messages (for email address authentication) are sent.
791          */
792        public SMTPTransportX.ConstructionContext mailTransferService()
793        {
794            return mailTransferService;
795        }
796
797
798            private final SMTPTransportX.ConstructionContext mailTransferService;
799
800
801    }
802
803
804
805//// P r i v a t e ///////////////////////////////////////////////////////////////////////
806
807
808    private void ensureWriteable( final File dir ) throws IOException
809    {
810        if( !dir.canWrite() ) // writeable by servlet container?
811        {
812            throw new IOException( "web interface (" + System.getProperty("user.name") + ") lacks write permissions for directory: " + dir ); // fail fast
813        }
814    }
815
816
817
818    /** Set at init start, cleared at end.
819      */
820    private final AtomicReference<Thread> initThreadA = new AtomicReference<Thread>();
821
822
823
824    private void mount( final NavBar bar, final HashSet<Class<? extends Page>> mountSet )
825    {
826        for( NavTab tab: bar.tabList() )
827        {
828            if( tab instanceof SuperTab )
829            {
830                mount( ((SuperTab)tab).subBar(), mountSet );
831                continue;
832            }
833
834            final Class<? extends Page> pageClass = tab.pageClass();
835            if( pageClass == null ) continue;
836
837            mount( pageClass, mountSet );
838        }
839    }
840
841
842
843    private void mount( final Class<? extends Page> pageClass,
844      final HashSet<Class<? extends Page>> mountSet )
845    {
846        if( pageClass.equals( WP_Server.class )) return; // home page, already mounted on /
847
848        if( !mountSet.add(pageClass) ) return; // already mounted
849
850        final StringBuilder pathB = new StringBuilder( pageClass.getSimpleName() );
851        if( pathB.indexOf("WP_") == 0 ) pathB.delete( 0, 3 ); // strip leading "WP_"
852
853        pathB.insert( 0, '/' );
854        mountPage( pathB.toString(), pageClass ); // creates a MountedMapper
855    }
856
857
858
859}