package 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. import java.io.*; import java.util.*; import java.util.concurrent.atomic.*; import java.util.logging.*; import java.util.regex.*; import java.sql.SQLException; import javax.script.*; import votorola.a.*; import votorola.a.response.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.logging.LoggerX; import votorola.g.script.*; import votorola.g.sql.*; /** A poll provided as a voter service. * * @see manual.xht#Polls */ public @ThreadRestricted("holds lock()") final class PollService extends VoterService implements Comparable, InputStore, Poll { // cf. a/trust/Trustserver private PollService( final VoteServer.Run run, JavaScriptIncluder s, final ConstructionContext cc ) throws IOException, ScriptException, SQLException { super( run, cc ); configurationScript = s; // Short name. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final int c = name.lastIndexOf('/') + 1; // or zero if there is none if( c > 0 && c != name.length() ) shortName = name.substring( c ); else shortName = name; // Responders. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final ArrayList responderList = new ArrayList(); responderList.add( new CR_Reconstruct( PollService.this )); responderList.add( new CR_Unvote( PollService.this )); responderList.add( new CR_Vote( PollService.this )); init( responderList ); constructionContext = null; // done with it, free the memory } private ConstructionContext cc() { return (ConstructionContext)constructionContext; } // nulled after init // ```````````````````````````````````````````````````````````````````````````````````` // init for early use private final File startupConfigurationFile = cc().startupConfigurationFile(); // ------------------------------------------------------------------------------------ /** The compiled configuration script for the poll. * * @see VoteServerScope#configurationFile() */ @Warning("thread restricted object") JavaScriptIncluder configurationScript() { assert lock.isHeldByCurrentThread(); // this method is safe, but not the object return configurationScript; } private final JavaScriptIncluder configurationScript; /** The current count to report, or null if there is none. * * @see votorola.a.count.Count.VoteServerScope#readyToReportLink() */ public Count countToReport() throws IOException, SQLException { assert lock.isHeldByCurrentThread(); final File readyToReportLink = vsRun.voteServer().scopeCount().readyToReportLink(); ReadyDirectory readyDirectory = null; // so far if( readyToReportLink.exists() ) { readyDirectory = new ReadyDirectory( readyToReportLink.getCanonicalPath() ); } if( readyDirectory == null ) { if( countToReport != null ) { logger.info( readyToReportLink + ": link is lost, stopping report: " + countToReport.readyDirectory() ); countToReport = null; } return countToReport; } if( !readyDirectory.isMounted() ) { logger.warning( readyToReportLink + ": count not mounted: " + readyDirectory ); countToReport = null; return countToReport; } if( countToReport == null || !countToReport.isObjectReadFromSerialFile( readyDirectory )) { logger.info( "starting new count report: " + readyDirectory + " (" + name + ")" ); try { countToReport = Count.readObjectFromSerialFile( name, readyDirectory ); } catch( FileNotFoundException x ) { logger.warning( "excluded from count: " + readyDirectory + " (" + name + ")" ); countToReport = null; return countToReport; } countToReport.init( new CountTable( readyDirectory, vsRun.database() )); } return countToReport; } private Count countToReport; // lazily set/reset through countToReport() /** Siezes the thread-access lock and returns the current count to report, or null if * there is none. This is just a thread safe wrapper that automatically siezes and * releases the lock. */ public @ThreadSafe Count countToReportT() throws IOException, SQLException { lock.lock(); try { return countToReport(); } finally { lock.unlock(); } } /** The set of divisions whose members are exclusively eligible to vote in this poll. * If the set is empty, then divisional membership is not an eligibility criterion. * Divisions of the local pollwiki are specified by page name, while all others are * specified by URL. * * @return unmodifiable set of zero or more pollwiki pagenames and/or URLs. * * @see ConstructionContext#addDivisionalComponent(String) * @see zelea.com/w/Property:Division#Divisions */ public @ThreadSafe Set divisionalComponents() { return divisionalComponents; } // FIX by removing to configuration script poll-service.js. Polls ought to be blind to // eligibility criteria. The script may set these properties internally (as it // does with pollTrustLevel, q.v.), if it actually needs them. private final Set divisionalComponents = Collections.unmodifiableSet( cc().divisionalComponents ); /** The polling division, specified by its pollwiki pagename. * * @return pollwiki pagename of division, or null if unspecified. * * @see ConstructionContext#setDivisionPageName(String) * @see zelea.com/w/Property:Division */ public @ThreadSafe String divisionPageName() { return divisionPageName; } private final String divisionPageName = cc().getDivisionPageName(); /** A small map of the polling division, specified by its pollwiki pagename. * * @return pollwiki pagename of division map, or null if unspecified. * * @see ConstructionContext#setDivisionSmallMapPageName(String) * @see zelea.com/w/Property:Small_map */ public @ThreadSafe String divisionSmallMapPageName() { return divisionSmallMapPageName; } private final String divisionSmallMapPageName = cc().getDivisionSmallMapPageName(); /** Either constructs an event to record the change that occured between oldVote and * newVote; or returns null, if no significant change occured. */ public ActivityEvent newChangeEventOrNull( final Vote oldVote, final Vote newVote ) { assert oldVote.voterEmail().equals( newVote.voterEmail() ); ActivityEvent event = null; if( newVote.getCandidateEmail() == null ) { if( oldVote.getCandidateEmail() != null ) { event = new Vote.WithdrawalEvent( PollService.this, oldVote, newVote ); } } else if( !newVote.getCandidateEmail().equals( oldVote.getCandidateEmail() )) { event = new Vote.CastEvent( PollService.this, newVote ); } return event; } /** The allowable pattern of a poll name. This imposes a further restriction on the * basic name pattern of voter services, in that the first character must be an * uppercase letter. * * @see #name() * @see #NAME_PATTERN * @see zelea.com/w/Category:Poll */ public static final Pattern POLL_NAME_PATTERN = Pattern.compile( "[A-Z](?:/?[A-Za-z0-9_.\\-])*" ); // cf. grep POLL_NAME_PATTERN // escaping the dash (\\-) is actually enough, no need to place it at end /** Converts a poll name to an XML name by replacing all instances of slashes '/' with * colons ':'. The result is valid for use in ID type attributes, but not for * element names, or anywhere else the colon is reserved for namespacing. * * @return the same b. * @see #xmlColonNameToPollName(StringBuilder) */ public static StringBuilder pollNameToXMLColonName( final StringBuilder b ) { // changing? change also in a/web/gwt/super/ for( int c = b.length() - 1; c >= 0; --c ) if( b.charAt(c) == '/' ) b.setCharAt( c, ':' ); return b; } /** The estimated number of eligible voters for this poll. * * @return number of eligible voters, or zero if unknown. * * @see ConstructionContext#setPopulationSize(long) * @see zelea.com/w/Property:Population size */ public @ThreadSafe long populationSize() { return populationSize; } private final long populationSize = cc().getPopulationSize(); /** An explanation of the population size. * * @see ConstructionContext#setPopulationSizeExplanation(String) * @see zelea.com/w/Property:Population size explanation */ public @ThreadSafe String populationSizeExplanation() { return populationSizeExplanation; } private final String populationSizeExplanation = cc().getPopulationSizeExplanation(); /** The short name of this poll, which is the part after the last slash '/' character. * If there is no slash character, or if the name ends with one, then the short name * is identical to the full name. * * @see #name */ public @ThreadSafe final String shortName() { return shortName; } private final String shortName; /** The wiki logo (wgLogo) image location for this poll, or null if none is specified. * * @see ConstructionContext#setWGLogoImageLocation(String) * @see www.mediawiki.org/wiki/Manual:$wgLogo */ public @ThreadSafe String wgLogoImageLocation() { return wgLogoImageLocation; } private final String wgLogoImageLocation = cc().getWGLogoImageLocation(); /** The wiki logo (wgLogo) link target for this poll. * * @see ConstructionContext#setWGLogoLinkTarget(String) * @see www.mediawiki.org/wiki/Manual:$wgLogo */ public @ThreadSafe String wgLogoLinkTarget() { return wgLogoLinkTarget; } private final String wgLogoLinkTarget = cc().getWGLogoLinkTarget(); /** Converts an XML name to a poll name by replacing all instances of colons ':' with * slashes '/'. * * @return the same b. * @see #pollNameToXMLColonName(StringBuilder) */ public static StringBuilder xmlColonNameToPollName( final StringBuilder b ) { // changing? change also in a/web/gwt/super/ for( int c = b.length() - 1; c >= 0; --c ) if( b.charAt(c) == ':' ) b.setCharAt( c, '/' ); return b; } // - C o m p a r a b l e -------------------------------------------------------------- /** Compares based on caseless short name, cased short name, caseless full name, and * finally cased full name. * * @see #name() * @see #shortName() */ public @ThreadSafe int compareTo( final PollService p ) { // cf. VoterService.equals final String shortNameP = p.shortName; int signum = shortName.compareToIgnoreCase( shortNameP ); if( signum == 0 ) { signum = shortName.compareTo( shortNameP ); if( signum == 0 ) { final String nameP = p.name; if( name.length() != shortName.length() || nameP.length() != shortNameP.length() ) { signum = name.compareToIgnoreCase( nameP ); if( signum == 0 ) signum = name.compareTo( nameP ); } } } return signum; } // - I n p u t - S t o r e ------------------------------------------------------------ public @ThreadSafe @Override InputTable voterInputTable() { return voterInputTable; } private final InputTable voterInputTable = new InputTable( PollService.this ); { voterInputTable.init(); } // - P o l l -------------------------------------------------------------------------- /** @see ConstructionContext#setDisplayTitle(String) */ public @ThreadSafe String displayTitle() { return displayTitle; } private final String displayTitle = cc().getDisplayTitle(); /** @see ConstructionContext#setIssueType(String) */ public @ThreadSafe String issueType() { return issueType; } private final String issueType = cc().getIssueType(); // - V o t e r - S e r v i c e -------------------------------------------------------- public @Override Exception dispatch( final String[] argArray, final CommandResponder.Session commandSession ) { voterInputTable.database().logAndClearWarnings(); try{ return super.dispatch( argArray, commandSession ); } finally{ voterInputTable.database().logAndClearWarnings(); } } /** @see VoteServerScope#configurationFile() */ public @ThreadSafe @Override File startupConfigurationFile() { return configurationScript.scriptFile(); } /** @see ConstructionContext#setSummaryDescription(String) */ public @ThreadSafe @Override String summaryDescription() { return summaryDescription; } private final String summaryDescription = cc().getSummaryDescription(); /** {@inheritDoc} The title is constructed from a combination of the {@linkplain * #name() name} and the {@linkplain #displayTitle() display title}, if any. */ public @ThreadSafe @Override String title() { return title; } private final String title; { final StringBuilder b = new StringBuilder(); b.append( name ); if( displayTitle != null ) { b.append( ": " ); b.append( displayTitle ); } title = b.toString(); } // ==================================================================================== /** A context for configuring the construction of a {@linkplain PollService PollService}. Each * construction is configured by the common {@linkplain PollService#configurationScript * configuration script} (s). During construction, an instance of this context * (pollCC) is passed to s via s::constructingPoll(pollCC). */ public static @ThreadRestricted("constructor") final class ConstructionContext extends VoterService.ConstructionContext { private ConstructionContext( VoteServer _voteServer, String _name, final JavaScriptIncluder s, boolean _isReconstruct ) throws IOException { super( _name, s, POLL_NAME_PATTERN ); isReconstruct = _isReconstruct; voteServer = _voteServer; summaryDescription = "This is a poll. Further information " + "is unavailable because the 'constructingPoll' function " + "of script " + s.scriptFile() + " " + "makes no call to 'setSummaryDescription'."; wgLogoLinkTarget = voteServer.pollwiki().uri().toASCIIString(); } // -------------------------------------------------------------------------------- /** Adds a division to the set of divisions whose members are exclusively eligible * to vote in this poll. The default set is empty, meaning that divisional * membership is not an eligibility criterion. * * @param div the pagename of a division as defined in the local * pollwiki, or the URL of a remotely defined division * @see PollService#divisionalComponents() */ public void addDivisionalComponent( final String div ) { divisionalComponents.add( div ); } private final HashSet divisionalComponents = new HashSet(); /** @see PollService#displayTitle() * @see #setDisplayTitle(String) */ public String getDisplayTitle() { return displayTitle; } private String displayTitle; /** Sets the display title of the poll. * * @see PollService#displayTitle() */ public void setDisplayTitle( String _displayTitle ) { displayTitle = _displayTitle; } /** @see PollService#divisionPageName() * @see #setDivisionPageName(String) */ public String getDivisionPageName() { return divisionPageName; } private String divisionPageName; /** Sets the pollwiki pagename of the polling division. The default value is * null, meaning unspecified. * * @see PollService#divisionPageName() */ public void setDivisionPageName( String _divisionPageName ) { divisionPageName = _divisionPageName; } /** @see PollService#divisionSmallMapPageName() * @see #setDivisionSmallMapPageName(String) */ public String getDivisionSmallMapPageName() { return divisionSmallMapPageName; } private String divisionSmallMapPageName; /** Sets the pollwiki pagename of the polling divisionSmallMap. The default value is * null, meaning unspecified. * * @see PollService#divisionSmallMapPageName() */ public void setDivisionSmallMapPageName( String _divisionSmallMapPageName ) { divisionSmallMapPageName = _divisionSmallMapPageName; } /** @see PollService#issueType() * @see #setIssueType(String) */ public String getIssueType() { return issueType; } private String issueType = "Issue"; /** Sets the issue type of the poll. The default value is "Issue", meaning * unspecified. * * @see PollService#issueType() */ public void setIssueType( String _issueType ) { if( _issueType == null ) throw new NullPointerException(); issueType = _issueType; } /** @see PollService#populationSize() * @see #setPopulationSize(long) */ public long getPopulationSize() { return populationSize; } private long populationSize; // default zero = unknown, so unlikely to divide by it /** Sets the population base of this poll. The default value is zero, * meaning unknown. * * @see PollService#populationSize() */ public void setPopulationSize( long _populationSize ) { populationSize = _populationSize; } /** @see PollService#populationSizeExplanation() * @see #setPopulationSizeExplanation(String) */ public String getPopulationSizeExplanation() { return populationSizeExplanation; } private String populationSizeExplanation = "Estimated number of eligible voters."; /** Sets the explanation of the population base. The default value is * "Estimated number of eligible voters.". * * @see PollService#populationSizeExplanation() */ public void setPopulationSizeExplanation( String _populationSizeExplanation ) { populationSizeExplanation = _populationSizeExplanation; } /** The vote-server. */ public VoteServer voteServer() { return voteServer; } private final VoteServer voteServer; /** @see PollService#summaryDescription() * @see #setSummaryDescription(String) */ public String getSummaryDescription() { return summaryDescription; } private String summaryDescription; /** Sets the summaryDescription of the poll. The default value is a * placeholder with configuration instructions for the administrator. * * @see PollService#summaryDescription() */ public void setSummaryDescription( String _summaryDescription ) { summaryDescription = _summaryDescription; } /** @see PollService#wgLogoImageLocation() * @see #setWGLogoImageLocation(String) */ public String getWGLogoImageLocation() { return wgLogoImageLocation; } private String wgLogoImageLocation; // no easy way to obtain local URL of default image, till the web interface is running /** Sets the image location for the wiki logo. The default value is * null, meaning unspecified. * * @see PollService#wgLogoImageLocation() */ public void setWGLogoImageLocation( String _wgLogoImageLocation ) { wgLogoImageLocation = _wgLogoImageLocation; } /** @see PollService#wgLogoLinkTarget() * @see #setWGLogoLinkTarget(String) */ public String getWGLogoLinkTarget() { return wgLogoLinkTarget; } private String wgLogoLinkTarget; /** Sets the link target of the wiki logo. The default value is the * {@linkplain PollwikiVS#uri() base URI} of the wiki. * * @see PollService#wgLogoLinkTarget() */ public void setWGLogoLinkTarget( String _wgLogoLinkTarget ) { wgLogoLinkTarget = _wgLogoLinkTarget; } /** Answers whether scratch construction is being attempted, in which cached * configuration items are ignored. During scratch construction, scripts are * expected to report verbose progress (via 'print' commands) for the benefit of * the administrator or user who requested it. */ public boolean isReconstruct() { return isReconstruct; } private final boolean isReconstruct; } // ==================================================================================== /** API for all polls within the scope of a vote-server. * * @see VoteServer#scopePoll() */ public static @ThreadSafe final class VoteServerScope { /** Constructs a VoteServerScope. */ public @Warning( "non-API" ) VoteServerScope( VoteServer _voteServer, final VoteServer.ConstructionContext vsCC ) { voteServer = _voteServer; pollCacheCapacity = vsCC.getPollCacheCapacity(); configurationFile = new File( voteServer.votorolaDirectory(), "poll-service.js" ); } // -------------------------------------------------------------------------------- /** The configuration file for all polls. It is located at: * *

{@linkplain VoteServer#votorolaDirectory() * votorolaDirectory}/poll-service.js

* *

The language is JavaScript. There are restrictions on the {@linkplain * votorola.g.script.JavaScriptIncluder character encoding}.

* * @see PollService#configurationScript() * @see poll-service.js (example script) * @see ../manual.xht#poll-service.js */ File configurationFile() { return configurationFile; } private final File configurationFile; /** The maximum number of entries in the poll cache. The cache uses an LRU * algorithm, discarding "least recently used" entries to stay within this * capacity. * * @see votorola.a.VoteServer.ConstructionContext#setPollCacheCapacity(int) */ final int pollCacheCapacity() { return pollCacheCapacity; } private final int pollCacheCapacity; // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = /** API for all polls within the scope of a vote-server run. * * @see votorola.a.VoteServer.Run#scopePoll() */ public @ThreadSafe final class Run { /** Constructs a Run. */ public @Warning( "non-API" ) Run( final VoteServer.Run _vsRun ) { vsRun = _vsRun; wikiCacheLastChurnTimeA = new AtomicLong( // 1a before 1b vsRun.voteServer().pollwiki().cache().lastChurnTime() ); final float loadFactor = 0.75f; final int spareCapacity = 2; pollCache = new LinkedHashMap( // 1b /*initial capacity*/(int)( (pollCacheCapacity + spareCapacity) / loadFactor ), loadFactor, /*accessOrder, for LRU cache*/true ) { protected boolean removeEldestEntry( Map.Entry eldest ) { return size() > pollCacheCapacity; } }; } // ---------------------------------------------------------------------------- /** Returns a newly constructed poll, caching it for later retrieval. * * @param logWriter the writer for logging construction messages. When * non-null, {@linkplain ConstructionContext#isReconstruct() scratch * construction} is attempted. (The writer must be a PrintWriter for * sake of scripts, where conventional 'print' statements fail with * other writer types.) */ public PollService constructCachedPoll( final String name, final PrintWriter logWriter ) throws IOException, ScriptException, SQLException { final boolean isReconstruct = logWriter != null; logger.fine(( isReconstruct? "constructing poll from scratch: ":"constructing poll: ") + name ); final JavaScriptIncluder s = new JavaScriptIncluder( configurationFile ); final ScriptContext sContext = s.engine().getContext(); final Writer oldWriter; final Writer oldErrorWriter; if( isReconstruct ) { oldWriter = sContext.getWriter(); sContext.setWriter( logWriter ); oldErrorWriter = sContext.getErrorWriter(); sContext.setErrorWriter( logWriter ); } else oldWriter = oldErrorWriter = null; final ConstructionContext cc = new ConstructionContext( voteServer, name, s, isReconstruct ); s.invokeKnownFunction( "constructingPoll", cc ); final PollService poll = new PollService( vsRun, s, cc ); if( isReconstruct ) // restore old writers { sContext.setWriter( oldWriter ); sContext.setErrorWriter( oldErrorWriter ); } synchronized( Run.this ) { pollCache.put( name, poll ); } s.invokeKnownFunction( "pollConstructed", poll ); return poll; } /** Returns a poll, if necessary constructing and caching it for later * retrieval. Note that the poll cache will be automatically cleared if it * is detected that the wiki cache has been churned. * * @see WikiCache#lastChurnTime() * @see votorola.a.count.PollService.VoteServerScope#pollCacheCapacity() */ public PollService ensurePoll( final String name ) throws IOException, ScriptException, SQLException { if( name == null ) throw new NullPointerException(); PollService poll; { final long timeIs = vsRun.voteServer().pollwiki().cache().lastChurnTime(); final long timeWas = wikiCacheLastChurnTimeA.getAndSet( timeIs ); // If this detector can result redundant clearances, in some cases, // it can never result in a permanently skipped one. if( timeWas == timeIs ) synchronized( Run.this ) { poll = pollCache.get( name ); } else { logger.config( "clearing poll cache, as apparently wiki cache was churned" ); pollCache.clear(); poll = null; } } if( poll == null ) poll = constructCachedPoll( name, /*logWriter*/null ); // Non-atomic test/construction here. The construction of the poll may // therefore be duplicated if the same poll is requested by multiple // threads. This should not happen too often, and it will do no harm. // Freeing the lock during poll construction and config (very slow) is // more important. return poll; } /** The vote-server run. */ public VoteServer.Run vsRun() { return vsRun; } private final VoteServer.Run vsRun; //// P r i v a t e /////////////////////////////////////////////////////////////// private final AtomicLong wikiCacheLastChurnTimeA; private @ThreadRestricted("holds Run.this") final HashMap pollCache; } //// P r i v a t e /////////////////////////////////////////////////////////////////// private final VoteServer voteServer; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Logger logger = LoggerX.i( PollService.class ); }