001package votorola.s.wic.count; // Copyright 2008-2010, 2012-2013, 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.sql.SQLException; 005import java.util.*; 006import javax.script.ScriptException; 007import org.apache.wicket.*; 008import org.apache.wicket.markup.html.*; 009import org.apache.wicket.markup.html.basic.*; 010import org.apache.wicket.request.cycle.*; 011import org.apache.wicket.request.mapper.parameter.PageParameters; 012import votorola.a.*; 013import votorola.a.count.*; 014import votorola.a.voter.*; 015import votorola.a.web.wic.*; 016import votorola.g.*; 017import votorola.g.lang.*; 018import votorola.g.locale.*; 019import votorola.g.web.wic.*; 020import votorola.g.util.*; 021 022import static votorola.a.voter.IDPair.NOBODY; 023 024 025/** A poll overview page. A live {@linkplain votorola.s.gwt.stage.StageV stage view} of 026 * Crossforum Theatre tops the page. The bulk of the page is static HTML, including a 027 * description of the poll and a reconstruction link. The particular poll is specified 028 * by query parameter 'p'. Query parameters for this page are: 029 * 030 * <table class='definition' style='margin-left:1em'> 031 * <tr> 032 * <th class='key'>Key</th> 033 * <th>Value</th> 034 * <th>Default</th> 035 * </tr> 036 * <tr><td class='key'>p</td> 037 * 038 * <td>The name of the <a href='http://reluk.ca/w/Category:Poll' target='_top'>poll</a>. 039 * Slash characters (/) are technically not allowed here 040 * and may therefore be encoded as exclamation marks (!).</td> 041 * 042 * <td>Null, resulting in a 303 (see other) redirect that fills in the name of 043 * the {@linkplain Poll#TEST_POLL_NAME test poll}.</td> 044 * 045 * </tr> 046 * <tr><td class='key'>reconstruct</td> 047 * 048 * <td>Whether to construct the poll from scratch. A value of 'y' constructs the 049 * poll from scratch by ignoring any cached configuration items, while 'n' 050 * constructs it normally. Use 'y' after modifying the definition of the poll 051 * (or one of dependencies) on the wiki side, in order to see the effect on the 052 * server side immediately. Normally there would be a delay of indeterminate 053 * duration.</td> 054 * 055 * <td>'n'</td> 056 * 057 * </tr> 058 * </table> 059 * 060 * @see <a href='../../../../../../s/wic/count/WP_Poll.html' target='_top'>WP_Poll.html</a> 061 */ 062 @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent 063public final class WP_Poll extends VPageHTML implements TabbedPage 064{ 065 066 067 /** Constructs a WP_Poll. 068 */ 069 public WP_Poll( final PageParameters pP ) throws IOException, ScriptException, SQLException 070 // bookmarkable page iff constructor public & (default|PageParameter) 071 { 072 super( pP ); 073 final VRequestCycle cycle = VRequestCycle.get(); 074 075 final PollService poll; 076 final StringWriter logStringWriter; 077 { 078 final String name = Poll.U.toName( maybeRedirect_P( WP_Poll.class, pP, cycle )); 079 080 final String reconstruct = pP.get( "reconstruct" ).toString( "n" ); 081 if( "n".equals( reconstruct )) 082 { 083 logStringWriter = null; 084 poll = pollFor( name, cycle ); 085 } 086 else if( "y".equals( reconstruct )) 087 { 088 logStringWriter = new StringWriter(); 089 final PrintWriter logWriter = new PrintWriter( logStringWriter ); 090 poll = pollFor( name, cycle, logWriter ); 091 } 092 else 093 { 094 VSession.get().error( "improper value for page parameter 'reconstruct': " 095 + reconstruct ); 096 throw new RestartResponseException( new WP_Message() ); 097 } 098 } 099 final VSession session = VSession.get(); 100 final String pollName = poll.name(); 101 session.scopePoll().setLastName( pollName ); 102 103 // Write glue for the GWT stage module of Crossforum Theatre 104 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 105 { 106 final StringBuilder b = WC_Stage.appendLeader( session.user(), 107 VOWicket.get().vsRun().voteServer(), cycle ); 108 109 // s_gwt_stage_Stage_init, per WC_Stage below 110 // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 111 b.append( 112 "voGWTConfig.s_gwt_stage_Stage_init = function()" 113 + "{" ); 114 final IDPair actor = session.scopeVoterPage().getLastIDPair(); 115 if( !NOBODY.equals( actor )) 116 { 117 b.append( 118 "s_gwt_stage_Stage_setActorName( '" ); // consistent with WP_Votespace and Rank 119 b.append( actor.username() ).append( "' );" ); 120 } 121 b.append( 122 "s_gwt_stage_Stage_setDefaultPollName( '" ); 123 b.append( pollName ).append( "' );" 124 + "};" ); 125 126 // ` ` ` 127 add( new WC_Stage( "stage", "votorola.s.gwt.wic.CountIn", b, cycle )); 128 } 129 130 // RENDER VIEW 131 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 132 add( new WC_NavigationHead( "navHead", WP_Poll.this, cycle )); 133 add( new WC_WGLogo( "wgLogo", poll.wgLogoImageLocation(), 134 poll.wgLogoLinkTarget(), cycle )); 135 { 136 final String mapPageName = poll.divisionSmallMapPageName(); 137 add( mapPageName == null? newNullComponent( "divisionSmallMap" ): 138 new WC_DivisionSmallMap( "divisionSmallMap", poll.divisionPageName(), 139 mapPageName, cycle )); 140 final WC_NavPile navPile = new WC_NavPile( "navPile", navTab(cycle), cycle ); 141 navPile.setRenderBodyOnly( false ); // for sake of 'id' assigned in WP_Poll.html 142 add( navPile ); 143 } 144 145 // Title and summary description 146 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 147 final BundleFormatter bunW = cycle.bunW(); 148 { 149 final String title = bunW.l( "s.wic.count.WP_Poll.title" ); 150 add( new Label( "title", title + " - " + pollName )); 151 } 152 add( new Label( "hName", poll.name() )); 153 { 154 final String displayTitle = poll.displayTitle(); 155 if( displayTitle == null ) add( newNullComponent( "hDisplayTitle" )); 156 else add( new Label( "hDisplayTitle", ": " + displayTitle )); 157 } 158 add( new Label( "summaryDescription", poll.summaryDescription() )); 159 160 // Reconstruction 161 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 162 { 163 final PageParameters linkParameters = new PageParameters( pP ); 164 linkParameters.set( "reconstruct", "y" ); 165 166 final BookmarkablePageLinkX link = new BookmarkablePageLinkX( 167 "reconstruct", WP_Poll.class, linkParameters ); 168 link.setBody( bunW.l( "s.wic.count.WP_Poll.reconstruct" )); 169 link.add( AttributeModifier.replace( "title", 170 bunW.l( "s.wic.count.WP_Poll.reconstructTip" ))); 171 add( link ); 172 } 173 174 if( logStringWriter == null ) add( newNullComponent( "log" )); 175 else add( new Label( "log", logStringWriter.toString() )); 176 } 177 178 179 180 // ------------------------------------------------------------------------------------ 181 182 183 /** Fetches the poll correponding to the 'p' parameter. If the 'p' parameter is 184 * missing, it is set to the name of the {@linkplain Poll#TEST_POLL_NAME test poll}, 185 * and a 303 redirect exception is thrown. 186 * 187 * @see #pollFor(String,VRequestCycle) 188 */ 189 static PollService ensurePoll( final Class<? extends Page> pageClass, PageParameters pP, 190 final VRequestCycle cycle ) throws IOException, ScriptException, SQLException 191 { 192 final String name = Poll.U.toName( maybeRedirect_P( pageClass, pP, cycle )); 193 return pollFor( name, cycle ); 194 } 195 196 197 198 /** Effects a redirect in response to missing mandatory parameters. If the 'p' 199 * parameter is missing, it is set to the name of the {@linkplain 200 * Poll#TEST_POLL_NAME test poll} and a 303 (see other) redirect exception is 201 * thrown. 202 * 203 * <p>This method may also do a 301 (permanent) redirect in response to obsolete 204 * parameter values. Currently these include polls that had to be moved to new names 205 * for technical reasons.</p> 206 * 207 * @return the value of the 'p' parameter. 208 */ 209 public static String maybeRedirect_P( final Class<? extends Page> pageClass, 210 final PageParameters pP, final VRequestCycle cycle ) 211 { 212 final String p = pP.get( "p" ).toString(); 213 if( p == null ) 214 { 215 pP.set( "p", Poll.U.toQuery( Poll.TEST_POLL_NAME )); 216 throw new RedirectException( cycle.uriFor(pageClass,pP).toASCIIString(), 303 ); 217 } 218 219 String area = null; 220 if( p.equals( "BGE" )) area = "De"; 221 else if( p.equals( "de" )) area = "G"; 222 else if( p.equals( "edor" )) area = "G"; 223 else if( p.equals( "grfin" )) area = "Tor"; 224 else if( p.equals( "m" )) area = "Tor"; 225 else if( p.equals( "MetaGov" )) area = "G"; 226 else if( p.equals( "Piraten" )) area = "De"; 227 else if( p.equals( "sandbox" )) area = "G"; 228 else if( p.equals( "tsmp" )) area = "Tor"; 229 else if( p.equals( "tsmpp" )) area = "Tor"; 230 else if( p.equals( "vohall" )) area = "G"; 231 else if( p.equals( "vop" )) area = "G"; 232 else if( p.equals( "w19c" )) area = "Tor"; 233 else if( p.equals( "w20c" )) area = "Tor"; 234 else if( p.equals( "wpe" )) area = "G"; 235 if( area != null ) 236 { 237 pP.set( "p", area + "!p!" + p ); 238 throw new RedirectException( cycle.uriFor(pageClass,pP).toASCIIString(), 301 ); 239 } 240 241 return p; 242 } 243 244 245 246 /** Fetches the poll corresponding to the specified service name. 247 * 248 * @see #ensurePoll(Class,PageParameters,VRequestCycle) 249 * @see #pollOrNullFor(PageParameters,VRequestCycle) 250 * @see PollFetcher#poll() 251 */ 252 static PollService pollFor( final String name, final VRequestCycle cycle ) throws IOException, 253 ScriptException, SQLException 254 { 255 return pollFor( name, cycle, /*logWriter*/null ); 256 } 257 258 259 260 private static PollService pollFor( final String name, final VRequestCycle cycle, 261 final PrintWriter logWriter ) throws IOException, ScriptException, SQLException 262 { 263 final PollService.VoteServerScope.Run vsRunPS = VOWicket.get().vsRun().scopePoll(); 264 try 265 { 266 return logWriter == null? vsRunPS.ensurePoll( name ): 267 vsRunPS.constructCachedPoll( name, logWriter ); 268 } 269 catch( VoterService.IllegalNameException x ) 270 { 271 VSession.get().error( "Unable to fetch poll \"" + name + "\": " + x.toString() ); 272 throw new RestartResponseException( new WP_Message() ); 273 } 274 } 275 276 277 278 /** Fetches the poll corresponding to query parameter 'p', if it is specified. 279 * 280 * @return poll, or null if none is specified. 281 * 282 * @see #pollFor(String,VRequestCycle) 283 */ 284 static PollService pollOrNullFor( final PageParameters pP, final VRequestCycle cycle ) 285 throws IOException, ScriptException, SQLException 286 { 287 final String p = pP.get( "p" ).toString(); 288 return p == null? null: pollFor( Poll.U.toName(p), cycle ); 289 } 290 291 292 293 /** Returns the specified page parameters (pP) with a value for the poll parameter 294 * ('p') as recalled from the session. If a value is recalled but pP is null, then 295 * pP is automatically constructed. 296 * 297 * @param pP the parameter map, which may be null. 298 */ 299 static PageParameters withRecall_p( PageParameters pP ) 300 { 301 final String lastName = VSession.get().scopePoll().getLastName(); 302 if( lastName != null ) pP = PageParametersX.withSet( pP, "p", Poll.U.toQuery(lastName) ); 303 return pP; 304 } 305 306 307 308 // - T a b b e d - P a g e ------------------------------------------------------------ 309 310 311 /** @see #NAV_TAB 312 */ 313 public NavTab navTab( VRequestCycle cycle ) { return NAV_TAB; } 314 315 316 317 /** The navigation tab that fetches the poll overview page, an instance of WP_Poll. 318 */ 319 static final NavTab NAV_TAB = new PollTab(); 320 321 322 323 // ==================================================================================== 324 325 326 /** A serializeable container for a poll and its latest count. 327 */ 328 static @ThreadRestricted("wicket") final class PollFetcher implements Serializable 329 { 330 331 private static final long serialVersionUID = 0L; 332 333 334 335 /** Constructs a poll fetcher. 336 */ 337 PollFetcher( final PollService poll ) 338 { 339 pollName = poll.name(); 340 pollOrNull = poll; 341 } 342 343 344 345 /** Constructs a poll fetcher. 346 */ 347 PollFetcher( final String pollName, final VRequestCycle cycle ) 348 { 349 this.pollName = pollName; 350 poll( cycle ); // in anticipation, taking advantage of the cycle reference 351 } 352 353 354 355 private final String pollName; 356 357 358 359 // -------------------------------------------------------------------------------- 360 361 362 /** The latest count, or null if there is none. 363 */ 364 Count countToReport() 365 { 366 if( countOrNull == null && !"".equals(count_readyDirectoryPath) ) // for a newly constructed 'this', or deserialized one that had a count 367 { 368 try { countOrNull = poll().countToReportT(); } 369 catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } // IOx or SQLx, not much expected 370 371 final String path; 372 if( countOrNull == null ) path = ""; 373 else path = countOrNull.readyDirectory().getPath(); 374 375 if( count_readyDirectoryPath == null ) count_readyDirectoryPath = path; 376 else if( !count_readyDirectoryPath.equals( path )) 377 { 378 throw new VotorolaRuntimeException( 379 "The poll has been recounted since this page was constructed. FIX this to show the user a WP_Message, with a link to new page." ); 380 } 381 } 382 383 if( countOrNull != null && !countOrNull.readyDirectory().isMounted() ) 384 { 385 throw new VotorolaRuntimeException( 386 "The poll count has been unmounted since this page was constructed. FIX this to show the user a WP_Message, with a link to new page." ); 387 } 388 389 return countOrNull; 390 } 391 392 393 private transient Count countOrNull; 394 395 396 private String count_readyDirectoryPath; 397 398 399 400 /** The poll. 401 */ 402 PollService poll() { return poll( VRequestCycle.get() ); } 403 404 405 PollService poll( final VRequestCycle cycle ) 406 { 407 if( pollOrNull == null && pollName != null ) 408 { 409 try 410 { 411 pollOrNull = pollFor( pollName, cycle ); // for a newly constructed or deserialized 'this' 412 } 413 catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } // IOException, ScriptException, SQLException 414 } 415 416 return pollOrNull; 417 } 418 419 420 private transient PollService pollOrNull; 421 422 } 423 424 425 426 // ==================================================================================== 427 428 429 private static @ThreadSafe final class PollTab extends NavTab 430 { 431 432 public @Override Bookmark bookmark() 433 { 434 return new Bookmark( WP_Poll.class, withRecall_p(null) ); 435 } 436 437 438 public @Override Class<? extends Page> pageClass() { return WP_Poll.class; } 439 440 441 public @Override String shortTitle( VRequestCycle cycle ) 442 { 443 return cycle.bunW().l( "s.wic.count.WP_Poll.tab.shortTitle" ); 444 } 445 446 } 447 448 449 450 // ==================================================================================== 451 452 453 /** Session scope for instances of WP_Poll. 454 * 455 * @see VSession#scopePoll() 456 */ 457 public static @ThreadSafe class SessionScope implements Serializable 458 { 459 460 private static final long serialVersionUID = 0L; 461 462 463 464 /** Constructs a SessionScope. 465 */ 466 public SessionScope( VSession session ) { this.session = session; } 467 468 469 470 private final VSession session; 471 472 473 474 // -------------------------------------------------------------------------------- 475 476 477 /** The last poll-name fore-navigated to, or null if there is none. 478 * 479 * @see #setLastName(String) 480 */ 481 public String getLastName() { return lastName; } 482 483 // Maybe the only code that absolutely needs this is in the supertab page 484 // WP_CountEngine, which currently isn't rendered. 485 486 487 private volatile String lastName = null; 488 489 490 /** Sets the last poll-name fore-navigated to. 491 * 492 * @see #getLastName() 493 */ 494 public void setLastName( final String newLastName ) 495 { 496 final String oldLastName = lastName; // snapshot copy, for atomic test/return 497 if( ObjectX.nullEquals( newLastName, oldLastName )) return; 498 499 lastName = newLastName; 500 session.dirty(); // per Session API 501 } 502 503 504 505 // /** The last node fore-navigated to, or null. If the optional correction is 506 // * enabled and the current page is divisional, then ensureLast() will first be 507 // * called using the current page path; thus correcting for any back-navigation. 508 // * 509 // * @see #getLast() 510 // */ 511 // public DivisionalNode lastNodeDisplayed( final DivisionalStratum stratum, // cf. votorola.a.voter.WC_VoterNavigator.SessionScope.lastVoterEmailDisplayed 512 // final boolean correctForCurrent, final VRequestCycle cycle ) 513 // { 514 // DivisionalPath lastPath = null; 515 // if( correctForCurrent ) 516 // { 517 // final Page page = cycle.responsePage(); 518 // if( page instanceof DivisionalPage ) 519 // { 520 // lastPath = ensureLast( ((DivisionalPage)page).divisionalPath() ); 521 // } 522 // } 523 // if( lastPath == null ) lastPath = last; 524 // 525 // return stratum.getNodeOnPath( lastPath ); 526 // } 527 /// Ripped out of old divisional code. Saved here as an example of handling the back 528 /// button, if ever needed again. 529 530 531 } 532 533 534 535}