package votorola.s.line; // Copyright 2008-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.net.*; import java.util.*; import java.util.logging.*; import java.util.regex.*; import javax.mail.internet.*; import javax.xml.stream.*; import votorola.a.*; import votorola.a.response.line.*; import votorola.a.trust.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.logging.*; import votorola.g.net.*; import votorola.g.option.*; import votorola.g.script.*; /** Main class of the executable votrace - a tool to trace a neigbourhood * trust network and compile a residential register. * * @see votrace */ public final class VOTrace extends ResultsCompiler { // cf. a/count/VOCount /** Runs the tool from the command line. * * @param argv command line argument array */ public static void main( final String[] argv ) { logger.info( "votrace is running with arguments " + Arrays.toString( argv )); new VOTrace().run( argv ); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Logger logger = LoggerX.i( VOTrace.class ); private static ArrayList nominalReadyDirectories( final File votraceDirectory ) { final ArrayList list = new ArrayList(); final File[] snapDirectoryArray = FileX.listFilesNoNull( votraceDirectory, 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( "votrace", argv, optionMap ); if( optionMap.get("help").hasOccured() ) { System.out.print ( "Usage: votrace [--verbose] [--etc[=ready|mount|report|umount]] [--churn]\n" + " or votrace snap [--verbose] [--etc[=ready|mount|report|umount]] [--churn]\n" + " votrace ready [--verbose] [--etc[=mount|report|umount]]\n" + " ~/votorola/out/votrace/snap-YYYY-MD-S\n" + " votrace mount [--verbose] [--etc[=report|umount]]\n" + " [~/votorola/out/votrace/snap-YYYY-MD-S/readyTrace-S]\n" + " votrace report [--verbose] [--etc]\n" + " [~/votorola/out/votrace/snap-YYYY-MD-S/readyTrace-S]\n" + " votrace umount [--verbose]\n" + " [~/votorola/out/votrace/snap-OLD-YYYY-MD-S/readyTrace-OLD-S]\n" + " or votrace ureport [--verbose]\n" + " or votrace status\n" + "Trace a neigbourhood trust network and compile a residential register.\n" ); return; } final int argCount = argv.length - aFirstNonOption; final Action action; final String argument; 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( "votrace: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "votrace" )); 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( "votrace: unrecognized action argument: " + actionArg ); System.err.println( GetoptX.createHelpPrompt( "votrace" )); 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( "votrace", optionMap ); final boolean isVerbose = optionMap.get("verbose").hasOccured(); try { final VoteServer.Run vsRun = vsRun(); final VoteServer vS = vsRun.voteServer(); final JavaScriptIncluder configScript = new JavaScriptIncluder( new File( vsRun.trustserver().serviceDirectory(), "votrace.js" )); // 1. Snap // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( action == Action.snap ) { if( argument != null ) { System.err.println( "votrace: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "votrace" )); System.exit( 1 ); } // timestamp santity check // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final File votraceDirectory = new File( vS.outDirectory(), "votrace" ); final long msStart = System.currentTimeMillis(); for( final File d: FileX.listFilesNoNull( votraceDirectory, OutputStore.SNAP_DIRECTORY_FILTER )) { if( d.lastModified() > msStart ) { System.err.println( "votrace, warning: existing snap directory timestamped in future: " + d ); // Problematic because the default for the 'ready' action // depends on timestamps. } } // churn // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( optionMap.get("churn").hasOccured() ) vS.pollwiki().cache().churn(); // snap // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final File snapDirectory = OutputStore.mkdirsY4MDS( votraceDirectory, "snap" ); final File inRegFile = new File( snapDirectory, "_in_registration.xml" ); final Spool outSpool = new Spool1(); try { final BufferedWriter w = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( inRegFile ), "UTF-8" )); outSpool.add( new Hold() { public void release() { try{ w.close(); } catch( IOException x ) { throw new RuntimeException( x ); }} }); w.append( " \n" + "\n" ); outSpool.add( new Hold() { public void release() { try{ w.append( "\t\n"); } catch( IOException x ) { throw new RuntimeException( x ); }} }); queryChain: for( String queryContinuation = "";; ) { final Spool inSpool = new Spool1(); try { final HttpURLConnection http; { // http://www.mediawiki.org/wiki/API:Query_-_Lists#categorymembers_.2F_cm final URI s = vS.pollwiki().scriptURI(); final URI queryURI = new URI( s.getScheme(), s.getAuthority(), s.getPath() + "/api.php", /*query*/"action=query&list=categorymembers&cmlimit=500&cmnamespace=2&cmprop=title&cmtitle=Category:Registrant&format=xml" + queryContinuation, /*fragment*/null ); logger.fine( "querying streetwiki " + queryURI ); http = (HttpURLConnection)( queryURI.toURL().openConnection() ); } URLConnectionX.connect( http ); inSpool.add( new Hold() { public void release() { http.disconnect(); } }); final InputStream in = http.getInputStream(); inSpool.add( new Hold() { public void release() { try{ in.close(); } catch( IOException x ) { throw new RuntimeException( x ); }} }); final XMLStreamReader r = MediaWiki.newXMLStreamReader( in, inSpool ); queryContinuation = null; while( r.hasNext() ) { r.next(); if( r.isStartElement() && "cm".equals( r.getLocalName() )) { final Registration reg; { final String ti = r.getAttributeValue( /*ns*/null, "title" ); final MatchResult m = MediaWiki.parsePageNameS( ti ); if( m == null ) { throw new VotorolaRuntimeException( "malformed page name '" + ti + "'" ); } final String username = m.group( 2 ); try { reg = new Registration( IDPair.fromUsername( username )); } catch( final AddressException x ) { logger.log( LoggerX.CONFIG, /*message*/"malformed username, ignoring registrant: " + username, x ); continue; } } final Registration.SnapshotContext rSC = new Registration.SnapshotContext( vS, reg ); configScript.invokeKnownFunction( "snapRegistration", rSC ); try { reg.toXML( new VoterInputTable.XMLColumnAppender( w )); } catch( VoterInputTable.BadInputException x ) { logger.log( LoggerX.CONFIG, /*message*/"unable to serialize snapshot, ignoring registrant: " + reg.registrant().username(), x ); } } else if( r.isStartElement() && "categorymembers".equals( r.getLocalName() )) { final String cmcontinue = r.getAttributeValue( /*ns*/null, "cmcontinue" ); if( cmcontinue != null ) // also serves to gaurd against clobbering. Up to two elements are expected with this same name, only one of which has the sought for attribute. { queryContinuation = "&cmcontinue=" + cmcontinue; } } else if( r.isStartElement() && "error".equals( r.getLocalName() )) { throw new MediaWiki.APIError( r ); } } if( queryContinuation == null ) break queryChain; } finally{ inSpool.unwind(); } } } finally{ outSpool.unwind(); } if( !isEtc || isVerbose ) System.out.println( snapDirectory ); } // 2. Ready // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.ready ) { final File votraceDirectory = new File( vS.outDirectory(), "votrace" ); if( argument == null ) { File snapDirectory = null; // till found for( final File d: FileX.listFilesNoNull( votraceDirectory, OutputStore.SNAP_DIRECTORY_FILTER )) { if( snapDirectory == null || d.lastModified() > snapDirectory.lastModified() ) snapDirectory = d; } if( snapDirectory == null ) { System.err.println( "votrace: nothing to ready, no snap directory exists\n" + " try: votrace snap" ); System.exit( 1 ); } argument = snapDirectory.getPath(); if( isVerbose ) { System.out.println( "readying most recently modified snap directory: " + argument ); } } // timestamp santity check // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final long msStart = System.currentTimeMillis(); for( final File r: nominalReadyDirectories( votraceDirectory )) { if( r.lastModified() > msStart ) { System.err.println( "votrace, warning: existing record timestamped in future: " + r ); // Problematic because the default for the 'mount' action // depends on timestamps. } } // ready // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` final File snapDirectory = new File( argument ); final ReadyDirectory r = ReadyDirectory.createReadyDirectory( snapDirectory ); if( !isEtc || isVerbose ) System.out.println( r ); } // 3. Mount // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.mount ) { final File votraceDirectory = new File( vS.outDirectory(), "votrace" ); final ArrayList readyDirectoryList = nominalReadyDirectories( votraceDirectory ); 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( "votrace: nothing to mount, no snap/readyTrace record exists\n" + " try: votrace ready" ); System.exit( 1 ); } if( readyDirectory.isMounted() ) { System.err.println( "votrace: 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( "votrace: already mounted: " + nominalReadyPath ); 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( "votrace, warning: existing mount timestamped in future: " + r.mountedDirectory() ); // Problematic because the default for the 'report' action // depends on timestamps. } } // mount // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` readyDirectory.mount( vsRun.trustserver(), isVerbose ); } // 4. Report // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.report ) { final NetworkTrace.VoteServerScope scopeTrace = vS.scopeTrace(); final File readyToReportLink = scopeTrace.readyToReportLink(); if( argument == null ) { final File votraceDirectory = new File( vS.outDirectory(), "votrace" ); ReadyDirectory readyDirectory = null; // till found File nominalReadyDirectory = null; for( final File d: nominalReadyDirectories( votraceDirectory )) { 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( "votrace: nothing to report, no trace is mounted" ); System.exit( 1 ); } if( readyToReportLink.exists() && readyDirectory.equals( readyToReportLink.getCanonicalFile() )) { System.err.println( "votrace: 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( "votrace: already reported: " + nominalReadyPath ); System.exit( 1 ); } if( !readyDirectory.isMounted() ) { System.err.println( "votrace: cannot report, trace is not mounted\n" + " try: votrace mount " + nominalReadyPath ); System.exit( 1 ); } FileX.symlink( readyDirectory.getPath(), readyToReportLink.getPath() ); FileX.symlink( readyDirectory.snapDirectory().getName(), scopeTrace.snapToReportLink().getPath() ); } // 5. Umount // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.umount ) { final File readyToReportLink = vS.scopeTrace().readyToReportLink(); if( argument == null ) // unmount all { final File votraceDirectory = new File( vS.outDirectory(), "votrace" ); for( final File nominalReadyDirectory: nominalReadyDirectories( votraceDirectory )) { 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 trace " + argument ); } continue; } if( isVerbose ) System.out.println( "unmounting " + argument ); run( action, argument, optionMap ); } return; } else // unmount one { final String nominalReadyPath = argument; final ReadyDirectory readyDirectory = new ReadyDirectory( nominalReadyPath ); if( readyToReportLink.exists() && readyDirectory.equals( readyToReportLink.getCanonicalFile() )) { System.err.println( "votrace: cannot unmount, trace is currently reported\n" + " try: votrace ureport" ); System.exit( 1 ); } if( !readyDirectory.unmount( vsRun )) { System.err.println( "votrace: not mounted: " + nominalReadyPath ); System.exit( 1 ); } } } // *. Ureport // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.ureport ) { if( argument != null ) { System.err.println( "votrace: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "votrace" )); System.exit( 1 ); } final NetworkTrace.VoteServerScope scopeTrace = vS.scopeTrace(); final File readyToReportLink = scopeTrace.readyToReportLink(); if( !readyToReportLink.exists() ) { System.err.println( "votrace: 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( "votrace: unable to delete report link, please delete it manually: " + readyToReportLink ); } final File snapToReportLink = scopeTrace.snapToReportLink(); if( !snapToReportLink.delete() ) { System.err.println( "votrace: unable to delete snap link, please delete it manually: " + snapToReportLink ); } } // *. Status // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else if( action == Action.status ) { if( argument != null ) { System.err.println( "votrace: too many arguments" ); System.err.println( GetoptX.createHelpPrompt( "votrace" )); System.exit( 1 ); } final File votraceDirectory = new File( vS.outDirectory(), "votrace" ); final File readyDirectoryToReportOrNull; { final File readyToReportLink = vS.scopeTrace().readyToReportLink(); readyDirectoryToReportOrNull = readyToReportLink.exists()? readyToReportLink.getCanonicalFile(): null; } final File[] snapDirectoryArray = FileX.listFilesNoNull( votraceDirectory, 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( "votrace: fatal error" + Votorola.unmessagedDetails(x) + ": " ); // System.err.println( x ); x.printStackTrace( System.err ); System.exit( 1 ); } } }