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}