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}