001package votorola.a.count; // Copyright 2007-2010, 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.*; 005import java.util.concurrent.atomic.*; 006import java.util.logging.*; 007import java.util.regex.*; 008import java.sql.SQLException; 009import javax.script.*; 010import votorola.a.*; 011import votorola.a.response.*; 012import votorola.a.voter.*; 013import votorola.g.*; 014import votorola.g.io.*; 015import votorola.g.lang.*; 016import votorola.g.locale.*; 017import votorola.g.logging.LoggerX; 018import votorola.g.script.*; 019import votorola.g.sql.*; 020 021 022/** A poll provided as a voter service. 023 * 024 * @see <a href='../../../../../s/manual.xht#Polls' target='_top' 025 * >manual.xht#Polls</a> 026 */ 027public @ThreadRestricted("holds lock()") final class PollService extends VoterService 028 implements Comparable<PollService>, InputStore, Poll 029{ 030 031 // cf. a/trust/Trustserver 032 033 034 private PollService( final VoteServer.Run run, JavaScriptIncluder s, 035 final ConstructionContext cc ) throws IOException, ScriptException, SQLException 036 { 037 super( run, cc ); 038 configurationScript = s; 039 040 // Short name. 041 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 042 final int c = name.lastIndexOf('/') + 1; // or zero if there is none 043 if( c > 0 && c != name.length() ) shortName = name.substring( c ); 044 else shortName = name; 045 046 // Responders. 047 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 048 final ArrayList<CommandResponder> responderList = new ArrayList<CommandResponder>(); 049 responderList.add( new CR_Reconstruct( PollService.this )); 050 responderList.add( new CR_Unvote( PollService.this )); 051 responderList.add( new CR_Vote( PollService.this )); 052 init( responderList ); 053 054 constructionContext = null; // done with it, free the memory 055 } 056 057 058 059 private ConstructionContext cc() { return (ConstructionContext)constructionContext; } // nulled after init 060 061 062 063 // ```````````````````````````````````````````````````````````````````````````````````` 064 // init for early use 065 066 067 private final File startupConfigurationFile = cc().startupConfigurationFile(); 068 069 070 071 // ------------------------------------------------------------------------------------ 072 073 074 /** The compiled configuration script for the poll. 075 * 076 * @see VoteServerScope#configurationFile() 077 */ 078 @Warning("thread restricted object") JavaScriptIncluder configurationScript() 079 { 080 assert lock.isHeldByCurrentThread(); // this method is safe, but not the object 081 return configurationScript; 082 } 083 084 085 private final JavaScriptIncluder configurationScript; 086 087 088 089 /** The current count to report, or null if there is none. 090 * 091 * @see votorola.a.count.Count.VoteServerScope#readyToReportLink() 092 */ 093 public Count countToReport() throws IOException, SQLException 094 { 095 assert lock.isHeldByCurrentThread(); 096 final File readyToReportLink = vsRun.voteServer().scopeCount().readyToReportLink(); 097 ReadyDirectory readyDirectory = null; // so far 098 if( readyToReportLink.exists() ) 099 { 100 readyDirectory = new ReadyDirectory( readyToReportLink.getCanonicalPath() ); 101 } 102 if( readyDirectory == null ) 103 { 104 if( countToReport != null ) 105 { 106 logger.info( readyToReportLink + ": link is lost, stopping report: " + countToReport.readyDirectory() ); 107 countToReport = null; 108 } 109 return countToReport; 110 } 111 112 if( !readyDirectory.isMounted() ) 113 { 114 logger.warning( readyToReportLink + ": count not mounted: " + readyDirectory ); 115 countToReport = null; 116 return countToReport; 117 } 118 119 if( countToReport == null || !countToReport.isObjectReadFromSerialFile( readyDirectory )) 120 { 121 logger.info( "starting new count report: " + readyDirectory + " (" + name + ")" ); 122 try 123 { 124 countToReport = Count.readObjectFromSerialFile( name, readyDirectory ); 125 } 126 catch( FileNotFoundException x ) 127 { 128 logger.warning( "excluded from count: " + readyDirectory + " (" + name + ")" ); 129 countToReport = null; 130 return countToReport; 131 } 132 133 countToReport.init( new CountTable( readyDirectory, vsRun.database() )); 134 } 135 return countToReport; 136 } 137 138 139 private Count countToReport; // lazily set/reset through countToReport() 140 141 142 143 /** Siezes the thread-access lock and returns the current count to report, or null if 144 * there is none. This is just a thread safe wrapper that automatically siezes and 145 * releases the lock. 146 */ 147 public @ThreadSafe Count countToReportT() throws IOException, SQLException 148 { 149 lock.lock(); 150 try { return countToReport(); } 151 finally { lock.unlock(); } 152 } 153 154 155 156 /** The set of divisions whose members are exclusively eligible to vote in this poll. 157 * If the set is empty, then divisional membership is not an eligibility criterion. 158 * Divisions of the local pollwiki are specified by page name, while all others are 159 * specified by URL. 160 * 161 * @return unmodifiable set of zero or more pollwiki pagenames and/or URLs. 162 * 163 * @see ConstructionContext#addDivisionalComponent(String) 164 * @see <a href='http://reluk.ca/w/Property:Division#Divisions' 165 * >zelea.com/w/Property:Division#Divisions</a> 166 */ 167 public @ThreadSafe Set<String> divisionalComponents() { return divisionalComponents; } 168 169 // FIX by removing to configuration script poll-service.js. Polls ought to be blind to 170 // eligibility criteria. The script may set these properties internally (as it 171 // does with pollTrustLevel, q.v.), if it actually needs them. 172 173 174 private final Set<String> divisionalComponents = 175 Collections.unmodifiableSet( cc().divisionalComponents ); 176 177 178 179 /** The polling division, specified by its pollwiki pagename. 180 * 181 * @return pollwiki pagename of division, or null if unspecified. 182 * 183 * @see ConstructionContext#setDivisionPageName(String) 184 * @see <a href='http://reluk.ca/w/Property:Division' 185 * >zelea.com/w/Property:Division</a> 186 */ 187 public @ThreadSafe String divisionPageName() { return divisionPageName; } 188 189 190 private final String divisionPageName = cc().getDivisionPageName(); 191 192 193 194 /** A small map of the polling division, specified by its pollwiki pagename. 195 * 196 * @return pollwiki pagename of division map, or null if unspecified. 197 * 198 * @see ConstructionContext#setDivisionSmallMapPageName(String) 199 * @see <a href='http://reluk.ca/w/Property:Small_map' 200 * >zelea.com/w/Property:Small_map</a> 201 */ 202 public @ThreadSafe String divisionSmallMapPageName() { return divisionSmallMapPageName; } 203 204 205 private final String divisionSmallMapPageName = cc().getDivisionSmallMapPageName(); 206 207 208 209 /** Either constructs an event to record the change that occured between oldVote and 210 * newVote; or returns null, if no significant change occured. 211 */ 212 public ActivityEvent newChangeEventOrNull( final Vote oldVote, final Vote newVote ) 213 { 214 assert oldVote.voterEmail().equals( newVote.voterEmail() ); 215 ActivityEvent event = null; 216 if( newVote.getCandidateEmail() == null ) 217 { 218 if( oldVote.getCandidateEmail() != null ) 219 { 220 event = new Vote.WithdrawalEvent( PollService.this, oldVote, newVote ); 221 } 222 } 223 else if( !newVote.getCandidateEmail().equals( oldVote.getCandidateEmail() )) 224 { 225 event = new Vote.CastEvent( PollService.this, newVote ); 226 } 227 return event; 228 } 229 230 231 232 /** The allowable pattern of a poll name. This imposes a further restriction on the 233 * basic name pattern of voter services, in that the first character must be an 234 * uppercase letter. 235 * 236 * @see #name() 237 * @see #NAME_PATTERN 238 * @see <a href='http://reluk.ca/w/Category:Poll' 239 * >zelea.com/w/Category:Poll</a> 240 */ 241 public static final Pattern POLL_NAME_PATTERN = Pattern.compile( 242 "[A-Z](?:/?[A-Za-z0-9_.\\-])*" ); // cf. grep POLL_NAME_PATTERN 243 // escaping the dash (\\-) is actually enough, no need to place it at end 244 245 246 247 /** Converts a poll name to an XML name by replacing all instances of slashes '/' with 248 * colons ':'. The result is valid for use in ID type attributes, but not for 249 * element names, or anywhere else the colon is reserved for namespacing. 250 * 251 * @return the same b. 252 * @see #xmlColonNameToPollName(StringBuilder) 253 */ 254 public static StringBuilder pollNameToXMLColonName( final StringBuilder b ) 255 { 256 // changing? change also in a/web/gwt/super/ 257 258 for( int c = b.length() - 1; c >= 0; --c ) if( b.charAt(c) == '/' ) b.setCharAt( c, ':' ); 259 return b; 260 } 261 262 263 264 /** The estimated number of eligible voters for this poll. 265 * 266 * @return number of eligible voters, or zero if unknown. 267 * 268 * @see ConstructionContext#setPopulationSize(long) 269 * @see <a href='http://reluk.ca/w/Property:Population_size' 270 * >zelea.com/w/Property:Population size</a> 271 */ 272 public @ThreadSafe long populationSize() { return populationSize; } 273 274 275 private final long populationSize = cc().getPopulationSize(); 276 277 278 279 /** An explanation of the population size. 280 * 281 * @see ConstructionContext#setPopulationSizeExplanation(String) 282 * @see <a href='http://reluk.ca/w/Property:Population_size_explanation' 283 * >zelea.com/w/Property:Population size explanation</a> 284 */ 285 public @ThreadSafe String populationSizeExplanation() { return populationSizeExplanation; } 286 287 288 private final String populationSizeExplanation = cc().getPopulationSizeExplanation(); 289 290 291 292 /** The short name of this poll, which is the part after the last slash '/' character. 293 * If there is no slash character, or if the name ends with one, then the short name 294 * is identical to the full name. 295 * 296 * @see #name 297 */ 298 public @ThreadSafe final String shortName() { return shortName; } 299 300 301 private final String shortName; 302 303 304 305 /** The wiki logo (wgLogo) image location for this poll, or null if none is specified. 306 * 307 * @see ConstructionContext#setWGLogoImageLocation(String) 308 * @see <a href='http://www.mediawiki.org/wiki/Manual:$wgLogo' 309 * >www.mediawiki.org/wiki/Manual:$wgLogo</a> 310 */ 311 public @ThreadSafe String wgLogoImageLocation() { return wgLogoImageLocation; } 312 313 314 private final String wgLogoImageLocation = cc().getWGLogoImageLocation(); 315 316 317 318 /** The wiki logo (wgLogo) link target for this poll. 319 * 320 * @see ConstructionContext#setWGLogoLinkTarget(String) 321 * @see <a href='http://www.mediawiki.org/wiki/Manual:$wgLogo' 322 * >www.mediawiki.org/wiki/Manual:$wgLogo</a> 323 */ 324 public @ThreadSafe String wgLogoLinkTarget() { return wgLogoLinkTarget; } 325 326 327 private final String wgLogoLinkTarget = cc().getWGLogoLinkTarget(); 328 329 330 331 /** Converts an XML name to a poll name by replacing all instances of colons ':' with 332 * slashes '/'. 333 * 334 * @return the same b. 335 * @see #pollNameToXMLColonName(StringBuilder) 336 */ 337 public static StringBuilder xmlColonNameToPollName( final StringBuilder b ) 338 { 339 // changing? change also in a/web/gwt/super/ 340 341 for( int c = b.length() - 1; c >= 0; --c ) if( b.charAt(c) == ':' ) b.setCharAt( c, '/' ); 342 return b; 343 } 344 345 346 347 // - C o m p a r a b l e -------------------------------------------------------------- 348 349 350 /** Compares based on caseless short name, cased short name, caseless full name, and 351 * finally cased full name. 352 * 353 * @see #name() 354 * @see #shortName() 355 */ 356 public @ThreadSafe int compareTo( final PollService p ) 357 { 358 // cf. VoterService.equals 359 final String shortNameP = p.shortName; 360 int signum = shortName.compareToIgnoreCase( shortNameP ); 361 if( signum == 0 ) 362 { 363 signum = shortName.compareTo( shortNameP ); 364 if( signum == 0 ) 365 { 366 final String nameP = p.name; 367 if( name.length() != shortName.length() || nameP.length() != shortNameP.length() ) 368 { 369 signum = name.compareToIgnoreCase( nameP ); 370 if( signum == 0 ) signum = name.compareTo( nameP ); 371 } 372 } 373 } 374 return signum; 375 } 376 377 378 379 // - I n p u t - S t o r e ------------------------------------------------------------ 380 381 382 public @ThreadSafe @Override InputTable voterInputTable() { return voterInputTable; } 383 384 385 private final InputTable voterInputTable = new InputTable( PollService.this ); 386 387 { 388 voterInputTable.init(); 389 } 390 391 392 393 // - P o l l -------------------------------------------------------------------------- 394 395 396 397 /** @see ConstructionContext#setDisplayTitle(String) 398 */ 399 public @ThreadSafe String displayTitle() { return displayTitle; } 400 401 402 private final String displayTitle = cc().getDisplayTitle(); 403 404 405 406 /** @see ConstructionContext#setIssueType(String) 407 */ 408 public @ThreadSafe String issueType() { return issueType; } 409 410 411 private final String issueType = cc().getIssueType(); 412 413 414 415 // - V o t e r - S e r v i c e -------------------------------------------------------- 416 417 418 public @Override Exception dispatch( final String[] argArray, 419 final CommandResponder.Session commandSession ) 420 { 421 voterInputTable.database().logAndClearWarnings(); 422 try{ return super.dispatch( argArray, commandSession ); } 423 finally{ voterInputTable.database().logAndClearWarnings(); } 424 } 425 426 427 428 /** @see VoteServerScope#configurationFile() 429 */ 430 public @ThreadSafe @Override File startupConfigurationFile() 431 { 432 return configurationScript.scriptFile(); 433 } 434 435 436 437 /** @see ConstructionContext#setSummaryDescription(String) 438 */ 439 public @ThreadSafe @Override String summaryDescription() { return summaryDescription; } 440 441 442 private final String summaryDescription = cc().getSummaryDescription(); 443 444 445 446 /** {@inheritDoc} The title is constructed from a combination of the {@linkplain 447 * #name() name} and the {@linkplain #displayTitle() display title}, if any. 448 */ 449 public @ThreadSafe @Override String title() { return title; } 450 451 452 private final String title; 453 454 { 455 final StringBuilder b = new StringBuilder(); 456 b.append( name ); 457 if( displayTitle != null ) 458 { 459 b.append( ": " ); 460 b.append( displayTitle ); 461 } 462 title = b.toString(); 463 } 464 465 466 467 // ==================================================================================== 468 469 470 /** A context for configuring the construction of a {@linkplain PollService PollService}. Each 471 * construction is configured by the common {@linkplain PollService#configurationScript 472 * configuration script} (s). During construction, an instance of this context 473 * (pollCC) is passed to s via s::constructingPoll(pollCC). 474 */ 475 public static @ThreadRestricted("constructor") final class ConstructionContext 476 extends VoterService.ConstructionContext 477 { 478 479 480 private ConstructionContext( VoteServer _voteServer, String _name, 481 final JavaScriptIncluder s, boolean _isReconstruct ) throws IOException 482 { 483 super( _name, s, POLL_NAME_PATTERN ); 484 isReconstruct = _isReconstruct; 485 voteServer = _voteServer; 486 487 summaryDescription = "This is a poll. Further information " 488 + "is unavailable because the 'constructingPoll' function " 489 + "of script " + s.scriptFile() + " " 490 + "makes no call to 'setSummaryDescription'."; 491 wgLogoLinkTarget = voteServer.pollwiki().uri().toASCIIString(); 492 } 493 494 495 496 // -------------------------------------------------------------------------------- 497 498 499 /** Adds a division to the set of divisions whose members are exclusively eligible 500 * to vote in this poll. The default set is empty, meaning that divisional 501 * membership is not an eligibility criterion. 502 * 503 * @param div the pagename of a division as defined in the local 504 * pollwiki, or the URL of a remotely defined division 505 * @see PollService#divisionalComponents() 506 */ 507 public void addDivisionalComponent( final String div ) { divisionalComponents.add( div ); } 508 509 510 private final HashSet<String> divisionalComponents = new HashSet<String>(); 511 512 513 514 /** @see PollService#displayTitle() 515 * @see #setDisplayTitle(String) 516 */ 517 public String getDisplayTitle() { return displayTitle; } 518 519 520 private String displayTitle; 521 522 523 /** Sets the display title of the poll. 524 * 525 * @see PollService#displayTitle() 526 */ 527 public void setDisplayTitle( String _displayTitle ) { displayTitle = _displayTitle; } 528 529 530 531 /** @see PollService#divisionPageName() 532 * @see #setDivisionPageName(String) 533 */ 534 public String getDivisionPageName() { return divisionPageName; } 535 536 537 private String divisionPageName; 538 539 540 /** Sets the pollwiki pagename of the polling division. The default value is 541 * null, meaning unspecified. 542 * 543 * @see PollService#divisionPageName() 544 */ 545 public void setDivisionPageName( String _divisionPageName ) 546 { 547 divisionPageName = _divisionPageName; 548 } 549 550 551 552 /** @see PollService#divisionSmallMapPageName() 553 * @see #setDivisionSmallMapPageName(String) 554 */ 555 public String getDivisionSmallMapPageName() { return divisionSmallMapPageName; } 556 557 558 private String divisionSmallMapPageName; 559 560 561 /** Sets the pollwiki pagename of the polling divisionSmallMap. The default value is 562 * null, meaning unspecified. 563 * 564 * @see PollService#divisionSmallMapPageName() 565 */ 566 public void setDivisionSmallMapPageName( String _divisionSmallMapPageName ) 567 { 568 divisionSmallMapPageName = _divisionSmallMapPageName; 569 } 570 571 572 573 /** @see PollService#issueType() 574 * @see #setIssueType(String) 575 */ 576 public String getIssueType() { return issueType; } 577 578 579 private String issueType = "Issue"; 580 581 582 /** Sets the issue type of the poll. The default value is "Issue", meaning 583 * unspecified. 584 * 585 * @see PollService#issueType() 586 */ 587 public void setIssueType( String _issueType ) 588 { 589 if( _issueType == null ) throw new NullPointerException(); 590 591 issueType = _issueType; 592 } 593 594 595 596 /** @see PollService#populationSize() 597 * @see #setPopulationSize(long) 598 */ 599 public long getPopulationSize() { return populationSize; } 600 601 602 private long populationSize; // default zero = unknown, so unlikely to divide by it 603 604 605 /** Sets the population base of this poll. The default value is zero, 606 * meaning unknown. 607 * 608 * @see PollService#populationSize() 609 */ 610 public void setPopulationSize( long _populationSize ) 611 { 612 populationSize = _populationSize; 613 } 614 615 616 617 /** @see PollService#populationSizeExplanation() 618 * @see #setPopulationSizeExplanation(String) 619 */ 620 public String getPopulationSizeExplanation() { return populationSizeExplanation; } 621 622 623 private String populationSizeExplanation = "Estimated number of eligible voters."; 624 625 626 /** Sets the explanation of the population base. The default value is 627 * "Estimated number of eligible voters.". 628 * 629 * @see PollService#populationSizeExplanation() 630 */ 631 public void setPopulationSizeExplanation( String _populationSizeExplanation ) 632 { 633 populationSizeExplanation = _populationSizeExplanation; 634 } 635 636 637 638 /** The vote-server. 639 */ 640 public VoteServer voteServer() { return voteServer; } 641 642 643 private final VoteServer voteServer; 644 645 646 647 /** @see PollService#summaryDescription() 648 * @see #setSummaryDescription(String) 649 */ 650 public String getSummaryDescription() { return summaryDescription; } 651 652 653 private String summaryDescription; 654 655 656 /** Sets the summaryDescription of the poll. The default value is a 657 * placeholder with configuration instructions for the administrator. 658 * 659 * @see PollService#summaryDescription() 660 */ 661 public void setSummaryDescription( String _summaryDescription ) 662 { 663 summaryDescription = _summaryDescription; 664 } 665 666 667 668 /** @see PollService#wgLogoImageLocation() 669 * @see #setWGLogoImageLocation(String) 670 */ 671 public String getWGLogoImageLocation() { return wgLogoImageLocation; } 672 673 674 private String wgLogoImageLocation; // no easy way to obtain local URL of default image, till the web interface is running 675 676 677 /** Sets the image location for the wiki logo. The default value is 678 * null, meaning unspecified. 679 * 680 * @see PollService#wgLogoImageLocation() 681 */ 682 public void setWGLogoImageLocation( String _wgLogoImageLocation ) 683 { 684 wgLogoImageLocation = _wgLogoImageLocation; 685 } 686 687 688 689 /** @see PollService#wgLogoLinkTarget() 690 * @see #setWGLogoLinkTarget(String) 691 */ 692 public String getWGLogoLinkTarget() { return wgLogoLinkTarget; } 693 694 695 private String wgLogoLinkTarget; 696 697 698 /** Sets the link target of the wiki logo. The default value is the 699 * {@linkplain PollwikiVS#uri() base URI} of the wiki. 700 * 701 * @see PollService#wgLogoLinkTarget() 702 */ 703 public void setWGLogoLinkTarget( String _wgLogoLinkTarget ) 704 { 705 wgLogoLinkTarget = _wgLogoLinkTarget; 706 } 707 708 709 710 /** Answers whether scratch construction is being attempted, in which cached 711 * configuration items are ignored. During scratch construction, scripts are 712 * expected to report verbose progress (via 'print' commands) for the benefit of 713 * the administrator or user who requested it. 714 */ 715 public boolean isReconstruct() { return isReconstruct; } 716 717 718 private final boolean isReconstruct; 719 720 721 } 722 723 724 725 // ==================================================================================== 726 727 728 /** API for all polls within the scope of a vote-server. 729 * 730 * @see VoteServer#scopePoll() 731 */ 732 public static @ThreadSafe final class VoteServerScope 733 { 734 735 736 /** Constructs a VoteServerScope. 737 */ 738 public @Warning( "non-API" ) VoteServerScope( VoteServer _voteServer, 739 final VoteServer.ConstructionContext vsCC ) 740 { 741 voteServer = _voteServer; 742 pollCacheCapacity = vsCC.getPollCacheCapacity(); 743 744 configurationFile = new File( voteServer.votorolaDirectory(), "poll-service.js" ); 745 } 746 747 748 749 // -------------------------------------------------------------------------------- 750 751 752 /** The configuration file for all polls. It is located at: 753 * 754 * <p class='indent'>{@linkplain VoteServer#votorolaDirectory() 755 * votorolaDirectory}/poll-service.js</p> 756 * 757 * <p>The language is JavaScript. There are restrictions on the {@linkplain 758 * votorola.g.script.JavaScriptIncluder character encoding}.</p> 759 * 760 * @see PollService#configurationScript() 761 * @see <a href='../../../../../a/count/poll-service.js' 762 * >poll-service.js (example script)</a> 763 * @see <a href='../../../../../s/manual.xht#poll-service.js' 764 * >../manual.xht#poll-service.js</a> 765 */ 766 File configurationFile() { return configurationFile; } 767 768 769 private final File configurationFile; 770 771 772 773 /** The maximum number of entries in the poll cache. The cache uses an LRU 774 * algorithm, discarding "least recently used" entries to stay within this 775 * capacity. 776 * 777 * @see votorola.a.VoteServer.ConstructionContext#setPollCacheCapacity(int) 778 */ 779 final int pollCacheCapacity() { return pollCacheCapacity; } 780 781 782 private final int pollCacheCapacity; 783 784 785 786 // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 787 788 789 /** API for all polls within the scope of a vote-server run. 790 * 791 * @see votorola.a.VoteServer.Run#scopePoll() 792 */ 793 public @ThreadSafe final class Run 794 { 795 796 797 /** Constructs a Run. 798 */ 799 public @Warning( "non-API" ) Run( final VoteServer.Run _vsRun ) 800 { 801 vsRun = _vsRun; 802 wikiCacheLastChurnTimeA = new AtomicLong( // 1a before 1b 803 vsRun.voteServer().pollwiki().cache().lastChurnTime() ); 804 final float loadFactor = 0.75f; 805 final int spareCapacity = 2; 806 pollCache = new LinkedHashMap<String,PollService>( // 1b 807 /*initial capacity*/(int)( (pollCacheCapacity + spareCapacity) / loadFactor ), 808 loadFactor, /*accessOrder, for LRU cache*/true ) 809 { 810 protected boolean removeEldestEntry( Map.Entry<String,PollService> eldest ) 811 { 812 return size() > pollCacheCapacity; 813 } 814 }; 815 } 816 817 818 819 // ---------------------------------------------------------------------------- 820 821 822 /** Returns a newly constructed poll, caching it for later retrieval. 823 * 824 * @param logWriter the writer for logging construction messages. When 825 * non-null, {@linkplain ConstructionContext#isReconstruct() scratch 826 * construction} is attempted. (The writer must be a PrintWriter for 827 * sake of scripts, where conventional 'print' statements fail with 828 * other writer types.) 829 */ 830 public PollService constructCachedPoll( final String name, final PrintWriter logWriter ) 831 throws IOException, ScriptException, SQLException 832 { 833 final boolean isReconstruct = logWriter != null; 834 logger.fine(( isReconstruct? "constructing poll from scratch: ":"constructing poll: ") + name ); 835 final JavaScriptIncluder s = new JavaScriptIncluder( configurationFile ); 836 final ScriptContext sContext = s.engine().getContext(); 837 final Writer oldWriter; final Writer oldErrorWriter; 838 if( isReconstruct ) 839 { 840 oldWriter = sContext.getWriter(); 841 sContext.setWriter( logWriter ); 842 oldErrorWriter = sContext.getErrorWriter(); 843 sContext.setErrorWriter( logWriter ); 844 } 845 else oldWriter = oldErrorWriter = null; 846 final ConstructionContext cc = new ConstructionContext( voteServer, name, s, 847 isReconstruct ); 848 s.invokeKnownFunction( "constructingPoll", cc ); 849 final PollService poll = new PollService( vsRun, s, cc ); 850 if( isReconstruct ) // restore old writers 851 { 852 sContext.setWriter( oldWriter ); 853 sContext.setErrorWriter( oldErrorWriter ); 854 } 855 synchronized( Run.this ) { pollCache.put( name, poll ); } 856 s.invokeKnownFunction( "pollConstructed", poll ); 857 return poll; 858 } 859 860 861 862 /** Returns a poll, if necessary constructing and caching it for later 863 * retrieval. Note that the poll cache will be automatically cleared if it 864 * is detected that the wiki cache has been churned. 865 * 866 * @see WikiCache#lastChurnTime() 867 * @see votorola.a.count.PollService.VoteServerScope#pollCacheCapacity() 868 */ 869 public PollService ensurePoll( final String name ) 870 throws IOException, ScriptException, SQLException 871 { 872 if( name == null ) throw new NullPointerException(); 873 874 PollService poll; 875 { 876 final long timeIs = vsRun.voteServer().pollwiki().cache().lastChurnTime(); 877 final long timeWas = wikiCacheLastChurnTimeA.getAndSet( timeIs ); 878 // If this detector can result redundant clearances, in some cases, 879 // it can never result in a permanently skipped one. 880 if( timeWas == timeIs ) synchronized( Run.this ) 881 { 882 poll = pollCache.get( name ); 883 } 884 else 885 { 886 logger.config( "clearing poll cache, as apparently wiki cache was churned" ); 887 pollCache.clear(); 888 poll = null; 889 } 890 } 891 if( poll == null ) poll = constructCachedPoll( name, /*logWriter*/null ); 892 // Non-atomic test/construction here. The construction of the poll may 893 // therefore be duplicated if the same poll is requested by multiple 894 // threads. This should not happen too often, and it will do no harm. 895 // Freeing the lock during poll construction and config (very slow) is 896 // more important. 897 return poll; 898 } 899 900 901 902 /** The vote-server run. 903 */ 904 public VoteServer.Run vsRun() { return vsRun; } 905 906 907 private final VoteServer.Run vsRun; 908 909 910 911 //// P r i v a t e /////////////////////////////////////////////////////////////// 912 913 914 private final AtomicLong wikiCacheLastChurnTimeA; 915 916 917 918 private @ThreadRestricted("holds Run.this") final HashMap<String,PollService> pollCache; 919 920 } 921 922 923 //// P r i v a t e /////////////////////////////////////////////////////////////////// 924 925 926 private final VoteServer voteServer; 927 928 929 } 930 931 932 933//// P r i v a t e /////////////////////////////////////////////////////////////////////// 934 935 936 private static final Logger logger = LoggerX.i( PollService.class ); 937 938 939}