001package votorola.s.wic.server; // 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.util.concurrent.TimeUnit;
005import java.util.logging.*; import votorola.g.logging.*;
006import java.util.List;
007import org.apache.wicket.*;
008import org.apache.wicket.markup.html.*;
009import org.apache.wicket.markup.html.basic.*;
010import org.apache.wicket.markup.repeater.*;
011import org.apache.wicket.model.*;
012import org.apache.wicket.request.mapper.parameter.PageParameters;
013import votorola.a.*;
014import votorola.a.count.*;
015import votorola.a.web.wic.*;
016import votorola.a.voter.*;
017import votorola.g.*;
018import votorola.g.hold.*;
019import votorola.g.io.*;
020import votorola.g.lang.*;
021import votorola.g.locale.*;
022import votorola.g.logging.*;
023import votorola.g.web.wic.*;
024import votorola.g.util.*;
025import votorola.s.wic.count.WP_Votespace;
026
027
028/** A vote-server activity page, showing a summary of user and administrative activity.
029  *
030  *     @see <a href='../../../../../../s/wic/server/WP_Activity.html' target='_top'>WP_Activity.html</a>
031  */
032  @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent
033public final class WP_Activity extends VPageHTML implements TabbedPage
034{
035
036
037    /** Constructs a WP_Activity.
038      */
039    public WP_Activity() // bookmarkable page iff constructor public & (default|PageParameter)
040    {
041        final VRequestCycle cycle = VRequestCycle.get();
042        final BundleFormatter bun = cycle.bunW();
043        final ApplicationScope appScope = VOWicket.get().scopeActivity();
044        add( new WC_NavigationHead( "navHead", WP_Activity.this, cycle ));
045        add( new WC_NavPile( "navPile", navTab(cycle), cycle ));
046
047      // Title
048      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
049        add( new Label( "title", NAV_TAB.shortTitle( cycle )));
050        add( new Label( "titleH", bun.l( "s.wic.server.WP_Activity.title" )));
051
052      // Activity window
053      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
054        add( new Label( "eventHeader-lapse", bun.l( "s.wic.server.WP_Activity.eventHeader-lapse" )));
055        add( new Label( "eventHeader-message", bun.l( "s.wic.server.WP_Activity.eventHeader-message" )));
056        {
057            final RepeatingView repeating = new RepeatingView( "eventData" );
058            synchronized( appScope.activityList() )
059            {
060                for( final ActivityEvent event: appScope.activityList() )
061                {
062                    final WebMarkupContainer tr = new WebMarkupContainer( repeating.newChildId() );
063
064                  // time lapse
065                  // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
066                    tr.add( new Label( "eventData-lapse", event.lapseToString( bun )));
067
068                  // message
069                  // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
070                    Component messageComponent;
071                    {
072                        final String id = "eventData-message";
073                        try
074                        {
075                            messageComponent = newMessageComponent( id , event, cycle );
076                        }
077                        catch( RuntimeException x ) { throw x; }
078                        catch( Exception x )
079                        {
080                            LoggerX.i(getClass()).log( LoggerX.WARNING, /*message*/"Unable to construct event", x );
081                            messageComponent = new Label( id,
082                              "Unable to construct event (see log for details): " + x.toString() );
083                        }
084                    }
085                    tr.add( messageComponent );
086
087                    repeating.add( tr );
088                }
089            }
090            add( repeating );
091        }
092
093      // Footer
094      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
095        {
096            final Label label = new Label( "footer", bun.l( "s.wic.server.WP_Activity.footer_XHT" ));
097            label.setEscapeModelStrings( false );
098            add( label );
099        }
100    }
101
102
103
104   // - T a b b e d - P a g e ------------------------------------------------------------
105
106
107    /** @see #NAV_TAB
108      */
109    public NavTab navTab( VRequestCycle cycle ) { return NAV_TAB; }
110
111
112
113    /** The navigation tab that fetches the vote-server activity page, an instance of
114      * WP_Activity.
115      */
116    public static final NavTab NAV_TAB = new NavTab()
117    {
118        private final Bookmark bookmark = new Bookmark( WP_Activity.class );
119        public @Override Bookmark bookmark() { return bookmark; }
120        public String shortTitle( VRequestCycle cycle )
121        {
122            return cycle.bunW().l( "s.wic.server.WP_Activity.tab.shortTitle" );
123        }
124    };
125
126
127
128   // ====================================================================================
129
130
131    /** Application scope for instances of WP_Activity.
132      *
133      *     @see VOWicket#scopeActivity()
134      */
135    public static @ThreadSafe final class ApplicationScope
136    {
137
138        /** Constructs an ApplicationScope.
139          */
140        public ApplicationScope( final VOWicket app )
141        {
142         // activityListFile = new File( app.outDirectory(), "web-activity-list.serial" );
143            activityListFile = new File( app.vsRun().voteServer().outDirectory(),
144              "web-activity-list.serial" );
145            ActivityList.TL list = null;
146            try
147            {
148                list = (ActivityList.TL)FileX.readObject( activityListFile );
149            }
150            catch( Exception x )
151            {
152                if( x instanceof RuntimeException ) throw (RuntimeException)x;
153                logger.config( "unable to restore activity list from previous run: " + x.toString() );
154            }
155
156            final long lastRunShutdownTime; // or 0L if unknown
157            if( list == null )
158            {
159                activityList = new ActivityList.TL();
160                lastRunShutdownTime = 0L;
161            }
162            else
163            {
164                activityList = list;
165                lastRunShutdownTime = activityListFile.lastModified();
166            }
167            activityList.log( new StartEvent( lastRunShutdownTime ));
168
169            app.executor().scheduleAtFixedRate( activityListWriter, // ensuring it is roughly up to date, even with abnormal termination abnormally
170              /*initial delay*/30, /*rate*/60, TimeUnit.MINUTES );
171            app.spool().add( new Hold()
172            {
173                public @ThreadSafe void release() { activityListWriter.run(); }
174            });
175
176        }
177
178
179        private final File activityListFile;
180
181
182        private final Runnable activityListWriter = new Runnable()
183        {
184            public @ThreadSafe void run()
185            {
186                synchronized( activityList )
187                {
188                    try
189                    {
190                        FileX.writeObject( activityList, activityListFile );
191                        activityListFile.setReadable( false, /*ownerOnly*/false ); // nobody can read/write, because it contains raw email addresses
192                        activityListFile.setWritable( false, /*ownerOnly*/false );
193                        activityListFile.setReadable( true, /*ownerOnly*/true ); // only owner can read/write
194                        activityListFile.setWritable( true, /*ownerOnly*/true );
195                    }
196                    catch( IOException x )
197                    {
198                        logger.config( "unable to persist activity list to file: " + x.toString() );
199                        activityListFile.delete();
200                    }
201                }
202            }
203        };
204
205
206       // --------------------------------------------------------------------------------
207
208
209          @Warning( "thread restricted object, holds activityList" )
210        public ActivityList.TL activityList() { return activityList; }
211
212
213            private final ActivityList.TL activityList;
214
215    }
216
217
218
219//// P r i v a t e ///////////////////////////////////////////////////////////////////////
220
221
222    private static final Logger logger = LoggerX.i( WP_Activity.class );
223
224
225
226    private Component newMessageComponent( final String id, final ActivityEvent event,
227      final VRequestCycle cycle ) throws Exception
228    {
229        final BundleFormatter bun = cycle.bunW();
230        final Label label = new Label( id, new Model<String>() );
231
232        if( event instanceof Vote.VotingEvent )
233        {
234            final Vote.VotingEvent votingEvent = (Vote.VotingEvent)event;
235            final VoteServer.Run run = VOWicket.get().vsRun();
236         // final VoterService serviceOrNull = run.voterService( votingEvent.pollName() ); // as provided on this run
237         /// it's always a poll, so:
238            final PollService poll = run.scopePoll().ensurePoll( votingEvent.pollName() );
239
240            label.setEscapeModelStrings( false );
241            appendStyleClass( label, "vote" );
242
243            final PageParameters linkParameters = new PageParameters();
244            final Class<? extends Page> linkClass;
245            final String pollTitle;
246         // if( serviceOrNull instanceof PollService )
247            {
248             // final PollService poll = (PollService)serviceOrNull;
249                pollTitle = poll.title();
250                linkClass = WP_Votespace.class;
251                linkParameters.set( "p", Poll.U.toQuery( poll.name() ));
252                final Count count = poll.countToReportT();
253                if( count == null || count.msStartSnap() <= votingEvent.timestamp() )
254                {
255                    appendStyleClass( label, "unreported" ); // vote not yet counted and reported
256                }
257            }
258         // else
259         // {
260         //     pollTitle = "Unprovided Service"; // rare case, maybe service was deleted
261         //     linkClass = WP_Voter.class;
262         //     appendStyleClass( label, "unprovided" );
263         // }
264
265            final String key;
266            if( event instanceof Vote.CastEvent )
267            {
268                key = "s.wic.server.WP_Activity.voteCastEvent_XHT";
269            }
270            else // WithdrawalEvent
271            {
272                assert event instanceof Vote.WithdrawalEvent;
273                key = "s.wic.server.WP_Activity.voteWithdrawalEvent_XHT";
274                appendStyleClass( label, "withdrawal" );
275            }
276
277            final String voterUsername = votingEvent.voter().username();
278            final String candidateUsername = votingEvent.candidate().username();
279            label.setDefaultModelObject( bun.l( key, pollTitle,
280              "<a href='" + urlFor( linkClass,
281                  PageParametersX.withSet( linkParameters, "u", voterUsername ))
282                + "' class='voter'>" + voterUsername + "</a>",
283              "<a href='" + urlFor( linkClass,
284                  PageParametersX.withSet( linkParameters, "u", candidateUsername ))
285                + "' class='candidate'>" + candidateUsername + "</a>" ));
286        }
287        else label.setDefaultModelObject( event.description( bun )); // default
288
289        return label;
290    }
291
292
293
294   // ====================================================================================
295
296
297    private static class StartEvent extends ActivityEvent
298    {
299
300        private static final long serialVersionUID = 0L;
301
302
303
304        private StartEvent( long lastRunShutdownTime )
305        {
306            this.lastRunShutdownTime = lastRunShutdownTime;
307        }
308
309
310
311        private final long lastRunShutdownTime; // or 0L if unknown
312
313
314
315       // - A c t i v i t y - E v e n t --------------------------------------------------
316
317
318        public final @Override String description( BundleFormatter bun )
319        {
320            if( lastRunShutdownTime > 0L )
321            {
322                return bun.l( "s.wic.server.WP_Activity.startEventLapse",
323                  lapseToString( lastRunShutdownTime, timestamp(), bun ));
324            }
325            else return bun.l( "s.wic.server.WP_Activity.startEvent" );
326        }
327
328
329
330    }
331
332
333
334}