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}