package votorola.s.line; // Copyright 2007-2011, 2013, 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.sql.*; import java.text.*; import java.util.*; import java.util.Date; // over java.sql.Date import votorola.a.*; import votorola.a.count.*; import votorola.a.response.line.*; import votorola.a.trust.NetworkTrace; import votorola.g.*; import votorola.g.hold.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.mail.*; import votorola.g.option.*; import votorola.g.sql.*; import votorola.g.text.*; /** Main class of the executable vocount - a tool to count the votes. * * @see vocount */ public @ThreadSafe final class VOCount extends ResultsCompiler { // cf. a/trust/VOTrace /** Runs the tool from the command line. * * @param argv command line argument array */ public static void main( final String[] argv ) { LoggerX.i(VOCount.class).info( "vocount is running with arguments " + Arrays.toString( argv )); new VOCount().run( argv ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private ArrayList nominalReadyDirectories( final File vocountDirectory ) { final ArrayList list = new ArrayList(); final File[] snapDirectoryArray = FileX.listFilesNoNull( vocountDirectory, OutputStore.SNAP_DIRECTORY_FILTER ); for( final File snapDirectory: snapDirectoryArray ) { final File[] readyDirectoryArray = FileX.listFilesNoNull( snapDirectory, ReadyDirectory.READY_DIRECTORY_FILTER ); for( final File readyDirectory: readyDirectoryArray ) list.add( readyDirectory ); } // Collections.sort( list ); // sorts on pathnames, with '/' messing up the order //// no need, it's fine on Linux return list; } private void run( final String[] argv ) { final Map optionMap = compileBaseOptions(); { final String name = "churn"; optionMap.put( name, new Option( name, Option.NO_ARGUMENT )); } final int aFirstNonOption = GetoptX.parse( "vocount", argv, optionMap ); if( optionMap.get("help").hasOccured() ) { System.out.print ( "Usage: vocount [--verbose] [--etc[=ready|mount|report|umount]] [--churn]\n" + " or vocount snap [--verbose] [--etc[=ready|mount|report|umount]]\n" + " vocount ready [--verbose] [--etc[=mount|report|umount]]\n" + " [~/votorola/out/vocount/snap-YYYY-MD-S]\n" + " vocount mount [--verbose] [--etc[=report|umount]] [--churn]\n" + " [~/votorola/out/vocount/snap-YYYY-MD-S/readyCount-S]\n" + " vocount report [--verbose] [--etc]\n" + " [~/votorola/out/vocount/snap-YYYY-MD-S/readyCount-S]\n" + " vocount umount [--verbose]\n" + " [~/votorola/out/vocount/snap-OLD-YYYY-MD-S/readyCount-OLD-S]\n" + " or vocount ureport [--verbose]\n" + " or vocount status\n" + "Count the votes and mark the results available for reporting.\n" ); return; } final int argCount = argv.length - aFirstNonOption; final Action action; // first argument final String argument; // second if( argCount == 0 ) { action = Action.snap; argument = null; final Option etc = optionMap.get( "etc" ); if( !etc.hasOccured() ) etc.addOccurence( Action.END_ACTION_LAST.name() ); } else if( argCount > 2 ) { System.err.println( "vocount: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "vocount" )); System.exit( 1 ); return; // redundant return, to prevent compiler warnings } else { final String actionArg = argv[aFirstNonOption]; try { action = Action.valueOf( actionArg ); } catch( IllegalArgumentException x ) { System.err.println( "vocount: unrecognized action argument: " + actionArg ); System.err.println( GetoptX.createHelpPrompt( "vocount" )); System.exit( 1 ); return; // redundant return, to prevent compiler warnings } if( argCount == 1 ) argument = null; else { assert argCount == 2; argument = argv[aFirstNonOption + 1]; } } run( action, argument, optionMap ); } private void run( final Action action, String argument, final Map optionMap ) { final boolean isEtc = action.isEtc( "vocount", optionMap ); final boolean isVerbose = optionMap.get("verbose").hasOccured(); try { final VoteServer vS = voteServer(); // 1. Snap // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( action == Action.snap ) { if( argument != null ) { System.err.println( "vocount: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "vocount" )); System.exit( 1 ); } // timestamp santity check // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final File vocountDirectory = new File( vS.outDirectory(), "vocount" ); final long msStart = System.currentTimeMillis(); for( final File d: FileX.listFilesNoNull( vocountDirectory, OutputStore.SNAP_DIRECTORY_FILTER )) { if( d.lastModified() > msStart ) { System.err.println( "vocount, warning: existing snap directory timestamped in future: " + d ); // Problematic because the default for the 'ready' action // depends on timestamps. } } // snap // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final File snapDirectory = OutputStore.mkdirsY4MDS( vocountDirectory, "snap" ); final File inVoteDirectory = new File( snapDirectory, "_in_vote" ); inVoteDirectory.mkdir(); final SimpleDateFormat iso8601Formatter = new SimpleDateFormat( SimpleDateFormatX.ISO_8601_PATTERN ); final Database database = vsRun().database(); try { synchronized( database ) { final Statement s = database.connection().createStatement(); try { Spool spool = Spool0.i(); final ResultSet r = s.executeQuery( "SELECT serviceName, voterEmail, xml FROM in_vote" + " ORDER BY serviceName" ); try { String serviceName = null; BufferedWriter w = null; final Date date = new Date( 0L ); while( r.next() ) { final String serviceNameKey = r.getString( 1 ); if( !serviceNameKey.equals( serviceName )) { spool.unwind(); spool = new Spool1(); serviceName = serviceNameKey; final File inVoteFile = new File( inVoteDirectory, serviceName + ".xml" ); // separate file per poll, to allow independent // verification of each by remote verifiers. // Also to keep file size within OS limits FileX.ensuredirs( inVoteFile.getParentFile() ); final BufferedWriter writerNew = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( inVoteFile ), "UTF-8" )); spool.add( new Hold() { public void release() { try { writerNew.append( " \n" ); writerNew.close(); } catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } } }); w = writerNew; w.append( " \n" + "\n" ); } final String voterEmail = r.getString(2); String loc = InternetAddressX.localPart( voterEmail ); String dom = voterEmail.substring( loc.length() + 1 ); w.append( " \n" + " \n" ); final Vote vote = new Vote( voterEmail, r.getString(3) ); date.setTime( vote.getTime() ); w.append( " \n" ); final String candidateEmail = vote.getCandidateEmail(); if( candidateEmail != null ) { loc = InternetAddressX.localPart( candidateEmail ); dom = candidateEmail.substring( loc.length() + 1 ); w.append( " \n" ); } w.append( " \n" + " \n" ); } } finally { spool.unwind(); r.close(); } } catch( SQLException x ) { if( !"42P01".equals( x.getSQLState() )) throw x; // 42P01 = UNDEFINED TABLE System.err.println( "vocount: OK, but no votes to count: " + x.toString() ); } finally{ s.close(); } } } finally{ database.logAndClearWarnings(); } // record snap.xml // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final BufferedWriter out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( new File( snapDirectory, OutputStore.SNAP_SUMMARY_FILENAME )), "UTF-8" )); try { out.append( "" ); } finally{ out.close(); } // ` ` ` if( !isEtc || isVerbose ) System.out.println( snapDirectory ); } // 2. Ready // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.ready ) { final File vocountDirectory = new File( vS.outDirectory(), "vocount" ); if( argument == null ) { File snapDirectory = null; // till found for( final File d: FileX.listFilesNoNull( vocountDirectory, OutputStore.SNAP_DIRECTORY_FILTER )) { if( snapDirectory == null || d.lastModified() > snapDirectory.lastModified() ) snapDirectory = d; } if( snapDirectory == null ) { System.err.println( "vocount: nothing to ready, no snap directory exists\n" + " try: vocount snap" ); System.exit( 1 ); } argument = snapDirectory.getPath(); if( isVerbose ) { System.out.println( "readying most recently modified snap directory: " + argument ); } } final File snapDirectory = new File( argument ); final File trustReadyToReportLink = vS.scopeTrace().readyToReportLink(); if( !trustReadyToReportLink.exists() ) { System.err.println( "vocount: no compiled registrations, missing symbolic link to ready trace: " + trustReadyToReportLink ); System.exit( 1 ); } // timestamp santity check // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long msStart = System.currentTimeMillis(); for( final File r: nominalReadyDirectories( vocountDirectory )) { if( r.lastModified() > msStart ) { System.err.println( "vocount, warning: existing record timestamped in future: " + r ); // Problematic because the default for the 'mount' action // depends on timestamps. } } // ready // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final File d = OutputStore.mkdirsS( snapDirectory, "readyCount" ); try { ReadyDirectory.createReadyDirectory( d, trustReadyToReportLink.getCanonicalPath()/*i.e. its target*/, vS.pollwiki().pipeRecognizer() ); } catch( final Exception x ) { if( !FileX.deleteRecursive( d )) { System.err.println( "vocount: unable to remove partially constructed ready directory, please delete it manually: " + d ); } throw x; } if( !isEtc || isVerbose ) System.out.println( d ); } // 3. Mount // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.mount ) { final File vocountDirectory = new File( vS.outDirectory(), "vocount" ); final ArrayList readyDirectoryList = nominalReadyDirectories( vocountDirectory ); if( argument == null ) { ReadyDirectory readyDirectory = null; // till found File nominalReadyDirectory = null; for( final File d: readyDirectoryList ) { final ReadyDirectory r = new ReadyDirectory( d ); // as such if( readyDirectory == null || r.lastModified() > readyDirectory.lastModified() ) { readyDirectory = r; nominalReadyDirectory = d; } } if( readyDirectory == null ) { System.err.println( "vocount: nothing to mount, no snap/readyCount record exists\n" + " try: vocount ready" ); System.exit( 1 ); } if( readyDirectory.isMounted() ) { System.err.println( "vocount: already mounted most recently modified record: " + nominalReadyDirectory ); System.exit( 1 ); } argument = nominalReadyDirectory.getPath(); if( isVerbose ) { System.out.println( "mounting most recently modified record: " + argument ); } } final String nominalReadyPath = argument; final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath ); if( readyDirectory.isMounted() ) { System.err.println( "vocount: already mounted: " + nominalReadyPath ); System.exit( 1 ); } final votorola.a.trust.ReadyDirectory trustReadyDirectory = new votorola.a.trust.ReadyDirectory( readyDirectory.readyTraceLink() ); if( !trustReadyDirectory.isMounted() ) { System.err.println( "vocount: cannot mount without a mounted trust network\n" + " try: votrace mount " + readyDirectory.readyTraceLink() ); System.exit( 1 ); } // timestamp santity check // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long msStart = System.currentTimeMillis(); for( final File d: readyDirectoryList ) { final ReadyDirectory r = new ReadyDirectory( d ); // as such if( !r.isMounted() ) continue; if( r.mountedDirectory().lastModified() > msStart ) { System.err.println( "vocount, warning: existing mount timestamped in future: " + r.mountedDirectory() ); // Problematic because the default for the 'report' action // depends on timestamps. } } // churn // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( optionMap.get("churn").hasOccured() ) vS.pollwiki().cache().churn(); // mount // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final NetworkTrace networkTrace = NetworkTrace.readObjectFromSerialFile( trustReadyDirectory ); if( readyDirectory.isInitWarned() ) { System.err.println( "vocount: mount despite logged warnings: " + nominalReadyPath ); } try { final int pollCount = readyDirectory.mount( vsRun(), networkTrace, isVerbose ); if( isVerbose ) { System.out.println(); System.out.print( pollCount ); System.out.print( " poll" ); if( pollCount != 1 ) System.out.print( 's' ); System.out.print( " counted" ); } } finally { if( isVerbose ) System.out.println(); } } // 4. Report // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.report ) { final Count.VoteServerScope scopeCount = vS.scopeCount(); final File readyToReportLink = scopeCount.readyToReportLink(); if( argument == null ) { final File vocountDirectory = new File( vS.outDirectory(), "vocount" ); ReadyDirectory readyDirectory = null; // till found File nominalReadyDirectory = null; for( final File d: nominalReadyDirectories( vocountDirectory )) { final ReadyDirectory r = new ReadyDirectory( d ); // as such if( !r.isMounted() ) continue; if( readyDirectory == null || r.mountedDirectory().lastModified() > readyDirectory.mountedDirectory().lastModified() ) { readyDirectory = r; nominalReadyDirectory = d; } } if( readyDirectory == null ) { System.err.println( "vocount: nothing to report, no count is mounted" ); System.exit( 1 ); } if( readyToReportLink.exists() && readyDirectory.equals( readyToReportLink.getCanonicalFile() )) { System.err.println( "vocount: already reporting most recently mounted record: " + nominalReadyDirectory ); System.exit( 1 ); } argument = nominalReadyDirectory.getPath(); if( isVerbose ) { System.out.println( "reporting most recently mounted record: " + argument ); } } final String nominalReadyPath = argument; final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath ); if( readyToReportLink.exists() && readyDirectory.equals( readyToReportLink.getCanonicalFile() )) { System.err.println( "vocount: already reported: " + nominalReadyPath ); System.exit( 1 ); } if( !readyDirectory.isMounted() ) { System.err.println( "vocount: cannot report, count is not mounted\n" + " try: vocount mount " + nominalReadyPath ); System.exit( 1 ); } FileX.symlink( readyDirectory.getPath(), readyToReportLink.getPath() ); FileX.symlink( readyDirectory.snapDirectory().getName(), scopeCount.snapToReportLink().getPath() ); } // 5. Umount // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.umount ) { final File readyToReportLink = vS.scopeCount().readyToReportLink(); if( argument == null ) // unmount all { final File vocountDirectory = new File( vS.outDirectory(), "vocount" ); for( final File nominalReadyDirectory: nominalReadyDirectories( vocountDirectory )) { final ReadyDirectory r = new ReadyDirectory( nominalReadyDirectory ); // as such if( !r.isMounted() ) continue; argument = nominalReadyDirectory.getPath(); if( r.equals( readyToReportLink.getCanonicalFile() )) { if( isVerbose ) { System.out.println( "skipping unmount of reported count " + argument ); } continue; } if( isVerbose ) System.out.println( "unmounting " + argument ); run( action, argument, optionMap ); } return; } else // unmount one, as specified { final String nominalReadyPath = argument; final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath ); if( readyToReportLink.exists() && readyDirectory.equals( readyToReportLink.getCanonicalFile() )) { System.err.println( "vocount: cannot unmount, count is currently reported\n" + " try: vocount ureport" ); System.exit( 1 ); } if( !readyDirectory.unmount( vsRun() )) { System.err.println( "vocount: not mounted: " + nominalReadyPath ); System.exit( 1 ); } } } // *. Ureport // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.ureport ) { if( argument != null ) { System.err.println( "vocount: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "vocount" )); System.exit( 1 ); } final Count.VoteServerScope scopeCount = vS.scopeCount(); final File readyToReportLink = scopeCount.readyToReportLink(); if( !readyToReportLink.exists()) { System.err.println( "vocount: nothing to unreport" ); System.exit( 1 ); } if( isVerbose ) { System.out.println( "unreporting " + readyToReportLink.getCanonicalFile() ); // difficult to recover the nominal path in this case, because getCanonicalFile() is the only easy way to de-reference the link } if( !readyToReportLink.delete() ) { System.err.println( "vocount: unable to delete report link, please delete it manually: " + readyToReportLink ); } final File snapToReportLink = scopeCount.snapToReportLink(); if( !snapToReportLink.delete() ) { System.err.println( "vocount: unable to delete snap link, please delete it manually: " + snapToReportLink ); } } // *. Status // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.status ) { if( argument != null ) { System.err.println( "vocount: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "vocount" )); System.exit( 1 ); } final File vocountDirectory = new File( vS.outDirectory(), "vocount" ); final File readyDirectoryToReportOrNull; { final File readyToReportLink = vS.scopeCount().readyToReportLink(); readyDirectoryToReportOrNull = readyToReportLink.exists()? readyToReportLink.getCanonicalFile(): null; } final File[] snapDirectoryArray = FileX.listFilesNoNull( vocountDirectory, OutputStore.SNAP_DIRECTORY_FILTER ); Arrays.sort( snapDirectoryArray ); final StringBuilder flagB = new StringBuilder( ReadyDirectory.STATUS_FLAGS_COUNT ); for( final File snapDirectory: snapDirectoryArray ) { final File[] readyDirectoryArray = FileX.listFilesNoNull( snapDirectory, ReadyDirectory.READY_DIRECTORY_FILTER ); if( readyDirectoryArray.length == 0 ) // unreadied snap { ReadyDirectory.statusPrintln( StringBuilderX.clear(flagB), snapDirectory ); continue; } Arrays.sort( readyDirectoryArray ); for( final File nominalReadyDirectory: readyDirectoryArray ) { final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyDirectory ); // as such readyDirectory.statusPrintln( StringBuilderX.clear(flagB), nominalReadyDirectory, readyDirectoryToReportOrNull ); } } } // - - - else assert false; if( isEtc ) { run( /*next*/Action.values()[action.ordinal() + 1], /*argument*/null, optionMap ); } } catch( RuntimeException x ) { throw x; } catch( Exception x ) { System.err.print( "vocount: fatal error" + Votorola.unmessagedDetails(x) + ": " ); // System.err.println( x ); x.printStackTrace( System.err ); System.exit( 1 ); } } }