package 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. import java.io.*; import java.util.concurrent.TimeUnit; import java.util.logging.*; import votorola.g.logging.*; import java.util.List; import org.apache.wicket.*; import org.apache.wicket.markup.html.*; import org.apache.wicket.markup.html.basic.*; import org.apache.wicket.markup.repeater.*; import org.apache.wicket.model.*; import org.apache.wicket.request.mapper.parameter.PageParameters; import votorola.a.*; import votorola.a.count.*; import votorola.a.web.wic.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.logging.*; import votorola.g.web.wic.*; import votorola.g.util.*; import votorola.s.wic.count.WP_Votespace; /** A vote-server activity page, showing a summary of user and administrative activity. * * @see WP_Activity.html */ @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent public final class WP_Activity extends VPageHTML implements TabbedPage { /** Constructs a WP_Activity. */ public WP_Activity() // bookmarkable page iff constructor public & (default|PageParameter) { final VRequestCycle cycle = VRequestCycle.get(); final BundleFormatter bun = cycle.bunW(); final ApplicationScope appScope = VOWicket.get().scopeActivity(); add( new WC_NavigationHead( "navHead", WP_Activity.this, cycle )); add( new WC_NavPile( "navPile", navTab(cycle), cycle )); // Title // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - add( new Label( "title", NAV_TAB.shortTitle( cycle ))); add( new Label( "titleH", bun.l( "s.wic.server.WP_Activity.title" ))); // Activity window // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - add( new Label( "eventHeader-lapse", bun.l( "s.wic.server.WP_Activity.eventHeader-lapse" ))); add( new Label( "eventHeader-message", bun.l( "s.wic.server.WP_Activity.eventHeader-message" ))); { final RepeatingView repeating = new RepeatingView( "eventData" ); synchronized( appScope.activityList() ) { for( final ActivityEvent event: appScope.activityList() ) { final WebMarkupContainer tr = new WebMarkupContainer( repeating.newChildId() ); // time lapse // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` tr.add( new Label( "eventData-lapse", event.lapseToString( bun ))); // message // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` Component messageComponent; { final String id = "eventData-message"; try { messageComponent = newMessageComponent( id , event, cycle ); } catch( RuntimeException x ) { throw x; } catch( Exception x ) { LoggerX.i(getClass()).log( LoggerX.WARNING, /*message*/"Unable to construct event", x ); messageComponent = new Label( id, "Unable to construct event (see log for details): " + x.toString() ); } } tr.add( messageComponent ); repeating.add( tr ); } } add( repeating ); } // Footer // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final Label label = new Label( "footer", bun.l( "s.wic.server.WP_Activity.footer_XHT" )); label.setEscapeModelStrings( false ); add( label ); } } // - T a b b e d - P a g e ------------------------------------------------------------ /** @see #NAV_TAB */ public NavTab navTab( VRequestCycle cycle ) { return NAV_TAB; } /** The navigation tab that fetches the vote-server activity page, an instance of * WP_Activity. */ public static final NavTab NAV_TAB = new NavTab() { private final Bookmark bookmark = new Bookmark( WP_Activity.class ); public @Override Bookmark bookmark() { return bookmark; } public String shortTitle( VRequestCycle cycle ) { return cycle.bunW().l( "s.wic.server.WP_Activity.tab.shortTitle" ); } }; // ==================================================================================== /** Application scope for instances of WP_Activity. * * @see VOWicket#scopeActivity() */ public static @ThreadSafe final class ApplicationScope { /** Constructs an ApplicationScope. */ public ApplicationScope( final VOWicket app ) { // activityListFile = new File( app.outDirectory(), "web-activity-list.serial" ); activityListFile = new File( app.vsRun().voteServer().outDirectory(), "web-activity-list.serial" ); ActivityList.TL list = null; try { list = (ActivityList.TL)FileX.readObject( activityListFile ); } catch( Exception x ) { if( x instanceof RuntimeException ) throw (RuntimeException)x; logger.config( "unable to restore activity list from previous run: " + x.toString() ); } final long lastRunShutdownTime; // or 0L if unknown if( list == null ) { activityList = new ActivityList.TL(); lastRunShutdownTime = 0L; } else { activityList = list; lastRunShutdownTime = activityListFile.lastModified(); } activityList.log( new StartEvent( lastRunShutdownTime )); app.executor().scheduleAtFixedRate( activityListWriter, // ensuring it is roughly up to date, even with abnormal termination abnormally /*initial delay*/30, /*rate*/60, TimeUnit.MINUTES ); app.spool().add( new Hold() { public @ThreadSafe void release() { activityListWriter.run(); } }); } private final File activityListFile; private final Runnable activityListWriter = new Runnable() { public @ThreadSafe void run() { synchronized( activityList ) { try { FileX.writeObject( activityList, activityListFile ); activityListFile.setReadable( false, /*ownerOnly*/false ); // nobody can read/write, because it contains raw email addresses activityListFile.setWritable( false, /*ownerOnly*/false ); activityListFile.setReadable( true, /*ownerOnly*/true ); // only owner can read/write activityListFile.setWritable( true, /*ownerOnly*/true ); } catch( IOException x ) { logger.config( "unable to persist activity list to file: " + x.toString() ); activityListFile.delete(); } } } }; // -------------------------------------------------------------------------------- @Warning( "thread restricted object, holds activityList" ) public ActivityList.TL activityList() { return activityList; } private final ActivityList.TL activityList; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Logger logger = LoggerX.i( WP_Activity.class ); private Component newMessageComponent( final String id, final ActivityEvent event, final VRequestCycle cycle ) throws Exception { final BundleFormatter bun = cycle.bunW(); final Label label = new Label( id, new Model() ); if( event instanceof Vote.VotingEvent ) { final Vote.VotingEvent votingEvent = (Vote.VotingEvent)event; final VoteServer.Run run = VOWicket.get().vsRun(); // final VoterService serviceOrNull = run.voterService( votingEvent.pollName() ); // as provided on this run /// it's always a poll, so: final PollService poll = run.scopePoll().ensurePoll( votingEvent.pollName() ); label.setEscapeModelStrings( false ); appendStyleClass( label, "vote" ); final PageParameters linkParameters = new PageParameters(); final Class linkClass; final String pollTitle; // if( serviceOrNull instanceof PollService ) { // final PollService poll = (PollService)serviceOrNull; pollTitle = poll.title(); linkClass = WP_Votespace.class; linkParameters.set( "p", Poll.U.toQuery( poll.name() )); final Count count = poll.countToReportT(); if( count == null || count.msStartSnap() <= votingEvent.timestamp() ) { appendStyleClass( label, "unreported" ); // vote not yet counted and reported } } // else // { // pollTitle = "Unprovided Service"; // rare case, maybe service was deleted // linkClass = WP_Voter.class; // appendStyleClass( label, "unprovided" ); // } final String key; if( event instanceof Vote.CastEvent ) { key = "s.wic.server.WP_Activity.voteCastEvent_XHT"; } else // WithdrawalEvent { assert event instanceof Vote.WithdrawalEvent; key = "s.wic.server.WP_Activity.voteWithdrawalEvent_XHT"; appendStyleClass( label, "withdrawal" ); } final String voterUsername = votingEvent.voter().username(); final String candidateUsername = votingEvent.candidate().username(); label.setDefaultModelObject( bun.l( key, pollTitle, "" + voterUsername + "", "" + candidateUsername + "" )); } else label.setDefaultModelObject( event.description( bun )); // default return label; } // ==================================================================================== private static class StartEvent extends ActivityEvent { private static final long serialVersionUID = 0L; private StartEvent( long lastRunShutdownTime ) { this.lastRunShutdownTime = lastRunShutdownTime; } private final long lastRunShutdownTime; // or 0L if unknown // - A c t i v i t y - E v e n t -------------------------------------------------- public final @Override String description( BundleFormatter bun ) { if( lastRunShutdownTime > 0L ) { return bun.l( "s.wic.server.WP_Activity.startEventLapse", lapseToString( lastRunShutdownTime, timestamp(), bun )); } else return bun.l( "s.wic.server.WP_Activity.startEvent" ); } } }