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}