001package 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.
002
003import java.io.*;
004import java.sql.*;
005import java.text.*;
006import java.util.*;
007import java.util.Date; // over java.sql.Date
008import votorola.a.*;
009import votorola.a.count.*;
010import votorola.a.response.line.*;
011import votorola.a.trust.NetworkTrace;
012import votorola.g.*;
013import votorola.g.hold.*;
014import votorola.g.io.*;
015import votorola.g.lang.*;
016import votorola.g.logging.*;
017import votorola.g.mail.*;
018import votorola.g.option.*;
019import votorola.g.sql.*;
020import votorola.g.text.*;
021
022
023/** Main class of the executable <code>vocount</code> - a tool to count the votes.
024  *
025  *     @see <a href='../../../../../s/manual.xht#line-vocount' target='_top'>vocount</a>
026  */
027public @ThreadSafe final class VOCount extends ResultsCompiler
028{
029
030    // cf. a/trust/VOTrace
031
032
033    /** Runs the tool from the command line.
034      *
035      *     @param argv command line argument array
036      */
037    public static void main( final String[] argv )
038    {
039        LoggerX.i(VOCount.class).info( "vocount is running with arguments " + Arrays.toString( argv ));
040        new VOCount().run( argv );
041    }
042
043
044
045//// P r i v a t e ///////////////////////////////////////////////////////////////////////
046
047
048    private ArrayList<File> nominalReadyDirectories( final File vocountDirectory )
049    {
050        final ArrayList<File> list = new ArrayList<File>();
051        final File[] snapDirectoryArray = FileX.listFilesNoNull( vocountDirectory,
052          OutputStore.SNAP_DIRECTORY_FILTER );
053        for( final File snapDirectory: snapDirectoryArray )
054        {
055            final File[] readyDirectoryArray = FileX.listFilesNoNull( snapDirectory,
056              ReadyDirectory.READY_DIRECTORY_FILTER );
057            for( final File readyDirectory: readyDirectoryArray ) list.add( readyDirectory );
058        }
059     // Collections.sort( list ); // sorts on pathnames, with '/' messing up the order
060     //// no need, it's fine on Linux
061        return list;
062    }
063
064
065
066    private void run( final String[] argv )
067    {
068        final Map<String,Option> optionMap = compileBaseOptions();
069        {
070            final String name = "churn";
071            optionMap.put( name, new Option( name, Option.NO_ARGUMENT ));
072        }
073
074        final int aFirstNonOption = GetoptX.parse( "vocount", argv, optionMap );
075        if( optionMap.get("help").hasOccured() )
076        {
077            System.out.print
078            (
079              "Usage: vocount [--verbose] [--etc[=ready|mount|report|umount]] [--churn]\n" +
080              "   or  vocount snap [--verbose] [--etc[=ready|mount|report|umount]]\n" +
081              "       vocount ready [--verbose] [--etc[=mount|report|umount]]\n" +
082              "          [~/votorola/out/vocount/snap-YYYY-MD-S]\n" +
083              "       vocount mount [--verbose] [--etc[=report|umount]] [--churn]\n" +
084              "          [~/votorola/out/vocount/snap-YYYY-MD-S/readyCount-S]\n" +
085              "       vocount report [--verbose] [--etc]\n" +
086              "          [~/votorola/out/vocount/snap-YYYY-MD-S/readyCount-S]\n" +
087              "       vocount umount [--verbose]\n" +
088              "          [~/votorola/out/vocount/snap-OLD-YYYY-MD-S/readyCount-OLD-S]\n" +
089              "   or  vocount ureport [--verbose]\n" +
090              "   or  vocount status\n" +
091              "Count the votes and mark the results available for reporting.\n"
092            );
093            return;
094        }
095
096        final int argCount = argv.length - aFirstNonOption;
097        final Action action; // first argument
098        final String argument; // second
099        if( argCount == 0 )
100        {
101            action = Action.snap;
102            argument = null;
103            final Option etc = optionMap.get( "etc" );
104            if( !etc.hasOccured() ) etc.addOccurence( Action.END_ACTION_LAST.name() );
105        }
106        else if( argCount > 2 )
107        {
108            System.err.println( "vocount: too many arguments" );
109            System.err.println( GetoptX.createHelpPrompt( "vocount" ));
110            System.exit( 1 ); return; // redundant return, to prevent compiler warnings
111        }
112        else
113        {
114            final String actionArg = argv[aFirstNonOption];
115            try
116            {
117                action = Action.valueOf( actionArg );
118            }
119            catch( IllegalArgumentException x )
120            {
121                System.err.println( "vocount: unrecognized action argument: " + actionArg );
122                System.err.println( GetoptX.createHelpPrompt( "vocount" ));
123                System.exit( 1 ); return; // redundant return, to prevent compiler warnings
124            }
125
126            if( argCount == 1 ) argument = null;
127            else
128            {
129                assert argCount == 2;
130                argument = argv[aFirstNonOption + 1];
131            }
132        }
133        run( action, argument, optionMap );
134    }
135
136
137
138    private void run( final Action action, String argument, final Map<String,Option> optionMap )
139    {
140        final boolean isEtc = action.isEtc( "vocount", optionMap );
141        final boolean isVerbose = optionMap.get("verbose").hasOccured();
142        try
143        {
144            final VoteServer vS = voteServer();
145
146          // 1. Snap
147          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
148            if( action == Action.snap )
149            {
150                if( argument != null )
151                {
152                    System.err.println( "vocount: too many arguments" );
153                    System.err.println( GetoptX.createHelpPrompt( "vocount" ));
154                    System.exit( 1 );
155                }
156
157              // timestamp santity check
158              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
159                final File vocountDirectory = new File( vS.outDirectory(), "vocount" );
160                final long msStart = System.currentTimeMillis();
161                for( final File d: FileX.listFilesNoNull( vocountDirectory,
162                  OutputStore.SNAP_DIRECTORY_FILTER ))
163                {
164                    if( d.lastModified() > msStart )
165                    {
166                        System.err.println(
167                          "vocount, warning: existing snap directory timestamped in future: " + d );
168                          // Problematic because the default for the 'ready' action
169                          // depends on timestamps.
170                    }
171                }
172
173              // snap
174              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
175                final File snapDirectory = OutputStore.mkdirsY4MDS( vocountDirectory, "snap" );
176                final File inVoteDirectory = new File( snapDirectory, "_in_vote" );
177                inVoteDirectory.mkdir();
178                final SimpleDateFormat iso8601Formatter =
179                  new SimpleDateFormat( SimpleDateFormatX.ISO_8601_PATTERN );
180                final Database database = vsRun().database();
181                try
182                {
183                    synchronized( database )
184                    {
185                        final Statement s = database.connection().createStatement();
186                        try
187                        {
188                            Spool spool = Spool0.i();
189                            final ResultSet r = s.executeQuery(
190                             "SELECT serviceName, voterEmail, xml FROM in_vote"
191                              + " ORDER BY serviceName" );
192                            try
193                            {
194                                String serviceName = null;
195                                BufferedWriter w = null;
196                                final Date date = new Date( 0L );
197                                while( r.next() )
198                                {
199                                    final String serviceNameKey = r.getString( 1 );
200                                    if( !serviceNameKey.equals( serviceName ))
201                                    {
202                                        spool.unwind();
203                                        spool = new Spool1();
204                                        serviceName = serviceNameKey;
205                                        final File inVoteFile = new File( inVoteDirectory,
206                                          serviceName + ".xml" );
207                                          // separate file per poll, to allow independent
208                                          // verification of each by remote verifiers.
209                                          // Also to keep file size within OS limits
210                                        FileX.ensuredirs( inVoteFile.getParentFile() );
211                                        final BufferedWriter writerNew = new BufferedWriter(
212                                          new OutputStreamWriter( new FileOutputStream(
213                                            inVoteFile ), "UTF-8" ));
214                                        spool.add( new Hold()
215                                        {
216                                            public void release()
217                                            {
218                                                try
219                                                {
220                                                    writerNew.append( "    </in>\n" );
221                                                    writerNew.close();
222                                                }
223                                                catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); }
224                                            }
225                                        });
226                                        w = writerNew;
227                                        w.append( "<?xml version='1.0' encoding='UTF-8'?> <!--                                  -*-coding: utf-8;-*- -->\n"
228                                                + "<in serviceName='" + serviceName + "'>\n" );
229                                    }
230                                    final String voterEmail = r.getString(2);
231                                    String loc = InternetAddressX.localPart( voterEmail );
232                                    String dom = voterEmail.substring( loc.length() + 1 );
233                                    w.append( "    <pos>\n"
234                                            + "        <subj dom='" + dom + "' loc='" + loc + "'/>\n" );
235                                    final Vote vote = new Vote( voterEmail, r.getString(3) );
236                                    date.setTime( vote.getTime() );
237                                    w.append( "        <vote t='" ).append( iso8601Formatter.format( date )).append( "'" );
238                                    final int dartSector = vote.getDartSector();
239                                    if( dartSector != 0 ) w.append( " dS='" ).append( Integer.toString(dartSector) ).append( "'" );
240                                    w.append( ">\n" );
241                                    final String candidateEmail = vote.getCandidateEmail();
242                                    if( candidateEmail != null )
243                                    {
244                                        loc = InternetAddressX.localPart( candidateEmail );
245                                        dom = candidateEmail.substring( loc.length() + 1 );
246                                        w.append(
247                                              "            <obj dom='" + dom + "' loc='" + loc + "'/>\n" );
248                                    }
249                                    w.append( "            </vote>\n"
250                                            + "        </pos>\n" );
251                                }
252                            }
253                            finally
254                            {
255                                spool.unwind();
256                                r.close();
257                            }
258                        }
259                        catch( SQLException x )
260                        {
261                            if( !"42P01".equals( x.getSQLState() )) throw x; // 42P01 = UNDEFINED TABLE
262
263                            System.err.println( "vocount: OK, but no votes to count: " + x.toString() );
264                        }
265                        finally{ s.close(); }
266                    }
267                }
268                finally{ database.logAndClearWarnings(); }
269
270              // record snap.xml
271              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
272                final BufferedWriter out = new BufferedWriter( new OutputStreamWriter(
273                  new FileOutputStream( new File( snapDirectory,
274                    OutputStore.SNAP_SUMMARY_FILENAME )), "UTF-8" ));
275                try
276                {
277                    out.append( "<snap msStart='" ); // a.count.Count.msStartSnap()
278                    out.append( Long.toString( msStart ));
279                    out.append( "' msEnd='" ); // a.count.Count.msEndSnap()
280                    out.append( Long.toString( System.currentTimeMillis() ));
281                    out.append(   "'/>" );
282                }
283                finally{ out.close(); }
284
285              // ` ` `
286                if( !isEtc || isVerbose ) System.out.println( snapDirectory );
287            }
288
289          // 2. Ready
290          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
291            else if( action == Action.ready )
292            {
293                final File vocountDirectory = new File( vS.outDirectory(), "vocount" );
294                if( argument == null )
295                {
296                    File snapDirectory = null; // till found
297                    for( final File d: FileX.listFilesNoNull( vocountDirectory,
298                      OutputStore.SNAP_DIRECTORY_FILTER ))
299                    {
300                        if( snapDirectory == null
301                         || d.lastModified() > snapDirectory.lastModified() ) snapDirectory = d;
302                    }
303                    if( snapDirectory == null )
304                    {
305                        System.err.println(
306                            "vocount: nothing to ready, no snap directory exists\n"
307                          + "    try: vocount snap" );
308                        System.exit( 1 );
309                    }
310
311                    argument = snapDirectory.getPath();
312                    if( isVerbose )
313                    {
314                        System.out.println( "readying most recently modified snap directory: "
315                          + argument );
316                    }
317                }
318                final File snapDirectory = new File( argument );
319                final File trustReadyToReportLink = vS.scopeTrace().readyToReportLink();
320                if( !trustReadyToReportLink.exists() )
321                {
322                    System.err.println(
323                      "vocount: no compiled registrations, missing symbolic link to ready trace: "
324                      + trustReadyToReportLink );
325                    System.exit( 1 );
326                }
327
328              // timestamp santity check
329              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
330                final long msStart = System.currentTimeMillis();
331                for( final File r: nominalReadyDirectories( vocountDirectory ))
332                {
333                    if( r.lastModified() > msStart )
334                    {
335                        System.err.println(
336                          "vocount, warning: existing record timestamped in future: " + r );
337                          // Problematic because the default for the 'mount' action
338                          // depends on timestamps.
339                    }
340                }
341
342              // ready
343              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
344                final File d = OutputStore.mkdirsS( snapDirectory, "readyCount" );
345                try
346                {
347                    ReadyDirectory.createReadyDirectory( d,
348                      trustReadyToReportLink.getCanonicalPath()/*i.e. its target*/,
349                      vS.pollwiki().pipeRecognizer() );
350                }
351                catch( final Exception x )
352                {
353                    if( !FileX.deleteRecursive( d ))
354                    {
355                        System.err.println(
356                          "vocount: unable to remove partially constructed ready directory, please delete it manually: "
357                          + d );
358                    }
359                    throw x;
360                }
361
362                if( !isEtc || isVerbose ) System.out.println( d );
363            }
364
365          // 3. Mount
366          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
367            else if( action == Action.mount )
368            {
369                final File vocountDirectory = new File( vS.outDirectory(), "vocount" );
370                final ArrayList<File> readyDirectoryList =
371                  nominalReadyDirectories( vocountDirectory );
372                if( argument == null )
373                {
374                    ReadyDirectory readyDirectory = null; // till found
375                    File nominalReadyDirectory = null;
376                    for( final File d: readyDirectoryList )
377                    {
378                        final ReadyDirectory r = new ReadyDirectory( d ); // as such
379                        if( readyDirectory == null || r.lastModified()
380                          > readyDirectory.lastModified() )
381                        {
382                            readyDirectory = r;
383                            nominalReadyDirectory = d;
384                        }
385                    }
386                    if( readyDirectory == null )
387                    {
388                        System.err.println(
389                            "vocount: nothing to mount, no snap/readyCount record exists\n"
390                          + "    try: vocount ready" );
391                        System.exit( 1 );
392                    }
393
394                    if( readyDirectory.isMounted() )
395                    {
396                        System.err.println(
397                          "vocount: already mounted most recently modified record: "
398                            + nominalReadyDirectory );
399                        System.exit( 1 );
400                    }
401
402                    argument = nominalReadyDirectory.getPath();
403                    if( isVerbose )
404                    {
405                        System.out.println( "mounting most recently modified record: " + argument );
406                    }
407                }
408                final String nominalReadyPath = argument;
409                final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath );
410                if( readyDirectory.isMounted() )
411                {
412                    System.err.println( "vocount: already mounted: " + nominalReadyPath );
413                    System.exit( 1 );
414                }
415
416                final votorola.a.trust.ReadyDirectory trustReadyDirectory =
417                  new votorola.a.trust.ReadyDirectory( readyDirectory.readyTraceLink() );
418                if( !trustReadyDirectory.isMounted() )
419                {
420                    System.err.println( "vocount: cannot mount without a mounted trust network\n"
421                                      + "    try: votrace mount "
422                                      + readyDirectory.readyTraceLink() );
423                    System.exit( 1 );
424                }
425
426              // timestamp santity check
427              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
428                final long msStart = System.currentTimeMillis();
429                for( final File d: readyDirectoryList )
430                {
431                    final ReadyDirectory r = new ReadyDirectory( d ); // as such
432                    if( !r.isMounted() ) continue;
433
434                    if( r.mountedDirectory().lastModified() > msStart )
435                    {
436                        System.err.println(
437                          "vocount, warning: existing mount timestamped in future: "
438                          + r.mountedDirectory() );
439                          // Problematic because the default for the 'report' action
440                          // depends on timestamps.
441                    }
442                }
443
444              // churn
445              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
446              if( optionMap.get("churn").hasOccured() ) vS.pollwiki().cache().churn();
447
448              // mount
449              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
450                final NetworkTrace networkTrace = NetworkTrace.readObjectFromSerialFile(
451                  trustReadyDirectory );
452                if( readyDirectory.isInitWarned() )
453                {
454                    System.err.println( "vocount: mount despite logged warnings: "
455                      + nominalReadyPath );
456                }
457                try
458                {
459                    final int pollCount = readyDirectory.mount( vsRun(), networkTrace, isVerbose );
460                    if( isVerbose )
461                    {
462                        System.out.println();
463                        System.out.print( pollCount );
464                        System.out.print( " poll" );
465                        if( pollCount != 1 ) System.out.print( 's' );
466                        System.out.print( " counted" );
467                    }
468                }
469                finally { if( isVerbose ) System.out.println(); }
470            }
471
472          // 4. Report
473          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
474            else if( action == Action.report )
475            {
476                final Count.VoteServerScope scopeCount = vS.scopeCount();
477                final File readyToReportLink = scopeCount.readyToReportLink();
478                if( argument == null )
479                {
480                    final File vocountDirectory = new File( vS.outDirectory(), "vocount" );
481                    ReadyDirectory readyDirectory = null; // till found
482                    File nominalReadyDirectory = null;
483                    for( final File d: nominalReadyDirectories( vocountDirectory ))
484                    {
485                        final ReadyDirectory r = new ReadyDirectory( d ); // as such
486                        if( !r.isMounted() ) continue;
487
488                        if( readyDirectory == null || r.mountedDirectory().lastModified()
489                          > readyDirectory.mountedDirectory().lastModified() )
490                        {
491                            readyDirectory = r;
492                            nominalReadyDirectory = d;
493                        }
494                    }
495
496                    if( readyDirectory == null )
497                    {
498                        System.err.println( "vocount: nothing to report, no count is mounted" );
499                        System.exit( 1 );
500                    }
501
502                    if( readyToReportLink.exists()
503                     && readyDirectory.equals( readyToReportLink.getCanonicalFile() ))
504                    {
505                        System.err.println(
506                          "vocount: already reporting most recently mounted record: "
507                          + nominalReadyDirectory );
508                        System.exit( 1 );
509                    }
510
511                    argument = nominalReadyDirectory.getPath();
512                    if( isVerbose )
513                    {
514                        System.out.println( "reporting most recently mounted record: " + argument );
515                    }
516                }
517                final String nominalReadyPath = argument;
518                final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath );
519                if( readyToReportLink.exists()
520                 && readyDirectory.equals( readyToReportLink.getCanonicalFile() ))
521                {
522                    System.err.println( "vocount: already reported: " + nominalReadyPath );
523                    System.exit( 1 );
524                }
525
526                if( !readyDirectory.isMounted() )
527                {
528                    System.err.println( "vocount: cannot report, count is not mounted\n"
529                                      + "    try: vocount mount " + nominalReadyPath );
530                    System.exit( 1 );
531                }
532
533                FileX.symlink( readyDirectory.getPath(), readyToReportLink.getPath() );
534                FileX.symlink( readyDirectory.snapDirectory().getName(),
535                  scopeCount.snapToReportLink().getPath() );
536            }
537
538          // 5. Umount
539          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
540            else if( action == Action.umount )
541            {
542                final File readyToReportLink = vS.scopeCount().readyToReportLink();
543                if( argument == null ) // unmount all
544                {
545                    final File vocountDirectory = new File( vS.outDirectory(), "vocount" );
546                    for( final File nominalReadyDirectory:
547                      nominalReadyDirectories( vocountDirectory ))
548                    {
549                        final ReadyDirectory r = new ReadyDirectory( nominalReadyDirectory ); // as such
550                        if( !r.isMounted() ) continue;
551
552                        argument = nominalReadyDirectory.getPath();
553                        if( r.equals( readyToReportLink.getCanonicalFile() ))
554                        {
555                            if( isVerbose )
556                            {
557                                System.out.println( "skipping unmount of reported count "
558                                  + argument );
559                            }
560                            continue;
561                        }
562
563                        if( isVerbose ) System.out.println( "unmounting " + argument );
564                        run( action, argument, optionMap );
565                    }
566                    return;
567                }
568                else // unmount one, as specified
569                {
570                    final String nominalReadyPath = argument;
571                    final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath );
572                    if( readyToReportLink.exists()
573                     && readyDirectory.equals( readyToReportLink.getCanonicalFile() ))
574                    {
575                        System.err.println( "vocount: cannot unmount, count is currently reported\n"
576                                          + "    try: vocount ureport" );
577                        System.exit( 1 );
578                    }
579
580                    if( !readyDirectory.unmount( vsRun() ))
581                    {
582                        System.err.println( "vocount: not mounted: " + nominalReadyPath );
583                        System.exit( 1 );
584                    }
585                }
586            }
587
588          // *. Ureport
589          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
590            else if( action == Action.ureport )
591            {
592                if( argument != null )
593                {
594                    System.err.println( "vocount: too many arguments" );
595                    System.err.println( GetoptX.createHelpPrompt( "vocount" ));
596                    System.exit( 1 );
597                }
598
599                final Count.VoteServerScope scopeCount = vS.scopeCount();
600                final File readyToReportLink = scopeCount.readyToReportLink();
601                if( !readyToReportLink.exists())
602                {
603                    System.err.println( "vocount: nothing to unreport" );
604                    System.exit( 1 );
605                }
606
607                if( isVerbose )
608                {
609                    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
610                }
611                if( !readyToReportLink.delete() )
612                {
613                    System.err.println(
614                      "vocount: unable to delete report link, please delete it manually: "
615                      + readyToReportLink );
616                }
617                final File snapToReportLink = scopeCount.snapToReportLink();
618                if( !snapToReportLink.delete() )
619                {
620                    System.err.println(
621                      "vocount: unable to delete snap link, please delete it manually: "
622                      + snapToReportLink );
623                }
624            }
625
626          // *. Status
627          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
628            else if( action == Action.status )
629            {
630                if( argument != null )
631                {
632                    System.err.println( "vocount: too many arguments" );
633                    System.err.println( GetoptX.createHelpPrompt( "vocount" ));
634                    System.exit( 1 );
635                }
636
637                final File vocountDirectory = new File( vS.outDirectory(), "vocount" );
638                final File readyDirectoryToReportOrNull;
639                {
640                    final File readyToReportLink = vS.scopeCount().readyToReportLink();
641                    readyDirectoryToReportOrNull = readyToReportLink.exists()?
642                        readyToReportLink.getCanonicalFile(): null;
643                }
644                final File[] snapDirectoryArray = FileX.listFilesNoNull( vocountDirectory,
645                  OutputStore.SNAP_DIRECTORY_FILTER );
646                Arrays.sort( snapDirectoryArray );
647                final StringBuilder flagB = new StringBuilder( ReadyDirectory.STATUS_FLAGS_COUNT );
648                for( final File snapDirectory: snapDirectoryArray )
649                {
650                    final File[] readyDirectoryArray = FileX.listFilesNoNull( snapDirectory,
651                      ReadyDirectory.READY_DIRECTORY_FILTER );
652                    if( readyDirectoryArray.length == 0 ) // unreadied snap
653                    {
654                        ReadyDirectory.statusPrintln( StringBuilderX.clear(flagB), snapDirectory );
655                        continue;
656                    }
657
658                    Arrays.sort( readyDirectoryArray );
659                    for( final File nominalReadyDirectory: readyDirectoryArray )
660                    {
661                        final ReadyDirectory readyDirectory =
662                          new ReadyDirectory( nominalReadyDirectory ); // as such
663                        readyDirectory.statusPrintln( StringBuilderX.clear(flagB),
664                          nominalReadyDirectory, readyDirectoryToReportOrNull );
665                    }
666                }
667            }
668
669          // - - -
670            else assert false;
671            if( isEtc )
672            {
673                run( /*next*/Action.values()[action.ordinal() + 1], /*argument*/null, optionMap );
674            }
675        }
676        catch( RuntimeException x ) { throw x; }
677        catch( Exception x )
678        {
679            System.err.print( "vocount: fatal error" + Votorola.unmessagedDetails(x) + ": " );
680         // System.err.println( x );
681            x.printStackTrace( System.err );
682            System.exit( 1 );
683        }
684    }
685
686
687}