package votorola.a.diff; // Copyright 2010-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 com.sun.jersey.api.uri.*; import java.io.*; import java.net.*; import java.util.*; import java.util.logging.*; import javax.ws.rs.core.*; import javax.xml.stream.*; import votorola.a.*; import votorola.a.count.*; import votorola.a.position.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.logging.*; import static votorola.a.position.DraftRevision.MAX_PATH_LENGTH; /** A pair of draft revisions for differencing. */ public final @ThreadSafe class DraftPair implements Serializable // where serialized? WP_D.pair { /** Constructs a DraftPair from a parsed difference key. * * @see #diffKeyParse() */ public static DraftPair newDraftPair( final DiffKeyParse diffKeyParse, final PollwikiVS wiki ) throws IOException { if( diffKeyParse.revisionSeries() != wiki.revisionSeries() ) { throw new RevisionSeriesMismatch( diffKeyParse, wiki ); } final List aPath = diffKeyParse.aPath(); final List bPath = diffKeyParse.bPath(); if( aPath.equals(bPath) ) { throw new MalformedSpecifier( "identical draft revision paths: " + aPath ); } // Construct the core revisions. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final List revList = new ArrayList<>( /*initial capacity*/2 ); final CoreRevision aCore; final CoreRevision bCore; { final int aRev = aPath.get( 0 ); final int bRev = bPath.get( 0 ); revList.add( aRev ); revList.add( bRev ); final LinkedList cores = new LinkedList<>(); CoreRevision.U.partialFromRevs( revList, cores, ComponentPipeRevision1.class, wiki, ThrowableX.ENLIST_NONE, /*toContinue*/false ); final CoreRevision core1 = cores.getFirst(); final CoreRevision core2 = cores.getLast(); // resolve unpredictable order of response by matching response revs against // those in request: if( core1.rev() == aRev ) { aCore = core1; bCore = core2; } else { aCore = core2; bCore = core1; } } // Initialize any component pipes in the cores by constructing their variants. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - revList.clear(); // reuse { final List components = new LinkedList<>(); enlistIfComponentPipe( aCore, components, aPath, /*variant*/revList ); enlistIfComponentPipe( bCore, components, bPath, /*variant*/revList ); final int rN = revList.size(); // 0..2 if( rN > 0 ) { // cf. ComponentPipeRevisionL.init2 final List variants = new LinkedList<>(); CoreRevision.U.partialFromRevs( revList, variants, /*componentPipeClass*/null, wiki, ThrowableX.ENLIST_NONE, /*toContinue*/false ); for( final CoreRevision variant: variants ) { final int variantRev = variant.rev(); // response order unpredictable, search for corresponding component(s): for( int r = 0; r < rN; ++r ) { if( revList.get(r) == variantRev ) components.get(r).init( variant ); // keep searching components, other may have same variant } } } } // Initialize any pointers among the cores and variants by constructing their remote drafts. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final HashMap listMap = new HashMap<>( // key remoteWikiScriptURI /*initial capacity*/(int)((2 + 1) / 0.75f) + 1 ); // per expected max remote wikis enlistAnyPointer( aCore, listMap, aPath ); enlistAnyPointer( bCore, listMap, bPath ); for( final PointerRevList pointerList: listMap.values() ) // each remote wiki { // cf. PointerRevision.initConsume final List draftRevList = pointerList.draftRevList; final int rN = draftRevList.size(); // 1..2 assert rN == pointerList.size(); // one per pointer final URI remoteWikiScriptURI = pointerList.remoteWikiScriptURI; final StringBuilder b = new StringBuilder(); b.append( remoteWikiScriptURI ); b.append( "/api.php?format=xml&action=query&revids=" ); for( int r = 0;; ) // append "rev" or "rev1|rev2" { b.append( draftRevList.get(r) ); ++r; if( r == rN ) break; b.append( "%7C" ); // vertical bar (|) } b.append( "&prop=info%7Crevisions&rvprop=ids" ); final URL queryURL = new URL( b.toString() ); logger.fine( "querying remote wiki for drafts: " + queryURL ); final Spool spool = new Spool1(); try { final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); for( ;; ) { final PageRevision remotePage = PageRevision1.readPageRevision( remoteWikiScriptURI, xml, /*toLatest*/false ); if( remotePage == null ) break; final int draftRev = remotePage.rev(); // response order unpredictable, search for corresponding pointer(s): for( int r = 0; r < rN; ++r ) { if( draftRevList.get(r) == draftRev ) { final PointerRevision pointer = pointerList.get( r ); pointer.init( new RemoteDraftRevision( pointer, remotePage.pageID(), remotePage.pageName(), remotePage.rev(), remotePage.revLatest() )); } // keep searching pointers, other may point to same remotePage } } } catch( final XMLStreamException x ) { throw new IOException( x ); } finally{ spool.unwind(); } } // Construct the draft pair. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - return new DraftPair( aCore.contextView(bCore.person().username()), bCore.contextView(aCore.person().username()), diffKeyParse, /*toTestPath*/true ); } /** Constructs a DraftPair from the latest revisions of the specified position cores. * The order of drafts in the resulting pair (aDraft, bDraft) will be lexical by * author name regardless of the order of actual parameters (pageName1, pageName2). * * @param countSource for possible use during this call, or null to construct one * internally. */ public static DraftPair newDraftPair( final String pageName1, final String pageName2, final VoteServer.Run vsRun, CountSource countSource ) throws IOException { if( pageName1.equals( pageName2 )) { throw new MalformedSpecifier( "identical core names: " + pageName1 ); } // Construct the core revisions. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final PollwikiVS wiki = vsRun.voteServer().pollwiki(); final CoreRevision aCore; final CoreRevision bCore; { final ArrayList pageNameList = new ArrayList<>( /*initial capacity*/2 ); pageNameList.add( pageName1 ); pageNameList.add( pageName2 ); final LinkedList cores = new LinkedList<>(); CoreRevision.U.partialFromPageNames( pageNameList, cores, ComponentPipeRevisionL.class, wiki, ThrowableX.ENLIST_NONE, /*toContinue*/false ); final CoreRevision core1 = cores.getFirst(); final CoreRevision core2 = cores.getLast(); // resolve unpredictable order of response by matching response page names // against those in request: if( DiffKey.isLexicallyOrdered( core1.person().username(), core2.person().username() )) { aCore = core1; bCore = core2; } else { aCore = core2; bCore = core1; } } // Initialize any component pipes in the cores by constructing their variants. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final List components = new LinkedList<>(); enlistIfComponentPipe( aCore, components ); enlistIfComponentPipe( bCore, components ); int c = components.size(); if( c > 0 ) { if( countSource == null) countSource = new CountSource1( vsRun ); for( ;; ) // each component { --c; final ComponentPipeRevisionL co = components.get( c ); co.init1( wiki, aCore.person().username(), countSource ); co.init1( wiki, bCore.person().username(), countSource ); if( c == 0 ) break; } ComponentPipeRevisionL.init2( components, wiki, ThrowableX.ENLIST_NONE, /*toContinue*/false ); } } // Initialize any pointers among the cores and variants by constructing their remote drafts. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final HashMap listMap = new HashMap<>( // key remoteWikiScriptURI /*initial capacity*/(int)((2 + 1) / 0.75f) + 1 ); // per expected max remote wikis enlistAnyPointer( aCore, listMap ); enlistAnyPointer( bCore, listMap ); for( final PointerList pointerList: listMap.values() ) // each remote wiki { PointerRevision.initConsume( pointerList, wiki, pointerList.remoteWikiScriptURI, /*toContinue*/false ); } } // Construct the draft pair. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - return newFromUncontextedViews( aCore, bCore, wiki ); } /** Constructs a DraftPair for the latest of each of the specified position cores that * resolves to a draft (aPageNameList) and appends it to the provided list of pairs. * Uses the latest of bPageName as the second core in each pair. Appends the pairs * in an unspecified order that might not correspond to the order of elements in * aPageNameList. * * @param aPageNameList the name list of first (a) core pages, ideally with spare * capacity to append one additional name. Duplicate names will not result in * duplicate pairs because the MediaWiki API collapses them to one. * @param countSource for possible use during this call, or null to construct one * internally. * @param userXList a pre-constructed list to which any user actionable * exceptions are to be appended, or null if none was pre-constructed. * * @return the same userXList if there was one; otherwise null if no * exceptions were encountered; otherwise a newly constructed list. */ public static List newDraftPairs( final ArrayList aPageNameList, final String bPageName, final VoteServer.Run vsRun, CountSource countSource, final List pairs, List userXList ) throws IOException { final int aN = aPageNameList.size(); if( aN == 0 ) return userXList; // nothing to do // Construct the core revisions. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final PollwikiVS wiki = vsRun.voteServer().pollwiki(); final List aCores = new LinkedList<>(); aPageNameList.add( bPageName ); // temporarily try { userXList = CoreRevision.U.partialFromPageNames( aPageNameList, aCores, ComponentPipeRevisionL.class, wiki, userXList, /*toContinue*/true ); } finally{ aPageNameList.remove( aN ); } // restore caller's list if( aCores.size() < 2 ) return userXList; // too many missing, no pair possible final CoreRevision bCore; for( final Iterator p = aCores.iterator();; ) { final CoreRevision core = p.next(); if( bPageName.equals( core.pageName() )) { p.remove(); bCore = core; break; } if( !p.hasNext() ) { return ThrowableX.listedThrowable( new MediaWiki.NoSuchPage( "cannot construct draft pairs against position core \"" + bPageName + "\", pollwiki has no such page", bPageName ), userXList ); } } // Initialize any component pipes among the cores by constructing their variants. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final List components = new LinkedList<>(); for( final CoreRevision aCore: aCores ) enlistIfComponentPipe( aCore, components ); enlistIfComponentPipe( bCore, components ); int c = components.size(); if( c > 0 ) { if( countSource == null) countSource = new CountSource1( vsRun ); for( ;; ) // each component { --c; final ComponentPipeRevisionL co = components.get( c ); for( final CoreRevision aCore: aCores ) { co.init1( wiki, aCore.person().username(), countSource ); } co.init1( wiki, bCore.person().username(), countSource ); if( c == 0 ) break; } ComponentPipeRevisionL.init2( components, wiki, userXList, /*toContinue*/true ); } } // Initialize any pointers among the cores and variants by constructing their remote drafts. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final HashMap listMap = new HashMap<>( // key remoteWikiScriptURI /*initial capacity*/(int)((4 + 1) / 0.75f) + 1 ); // per expected max remote wikis for( final CoreRevision aCore: aCores ) enlistAnyPointer( aCore, listMap ); enlistAnyPointer( bCore, listMap ); for( final PointerList pointerList: listMap.values() ) // each remote wiki { PointerRevision.initConsume( pointerList, wiki, pointerList.remoteWikiScriptURI, /*toContinue*/true ); } } // Construct the draft pairs. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( bCore.draft() == null ) { return ThrowableX.listedThrowable( new MalformedContent( "cannot construct pairs against bCore, it does not resolve to a draft", bCore ), userXList ); } for( final Iterator a = aCores.iterator(); a.hasNext(); ) { final CoreRevision aCore = a.next(); if( aCore.isFullyConstructed() ) { pairs.add( newFromUncontextedViews( aCore, bCore, wiki )); } } return userXList; } /** @param _aCore per {@linkplain #aCore() aCore}. It need not already be == * aCore.{@linkplain CoreRevision#contextView(String) contextView}(b). * @param _bCore per {@linkplain #bCore() bCore}. It need not already be == * bCore.{@linkplain CoreRevision#contextView(String) contextView}(a). */ private static DraftPair newFromUncontextedViews( CoreRevision aCore, CoreRevision bCore, final PollwikiVS wiki ) throws DiffKeyParse.MalformedKey { aCore = aCore.contextView( bCore.person().username() ); bCore = bCore.contextView( aCore.person().username() ); final DiffKeyParse diffKeyParse = new DiffKeyParse( aCore.addDraftRevisionPath( new ArrayList(MAX_PATH_LENGTH) ), bCore.addDraftRevisionPath( new ArrayList(MAX_PATH_LENGTH) ), wiki.revisionSeries() ); try { return new DraftPair( aCore, bCore, diffKeyParse, /*toTestPath*/false ); } catch( final PathMismatch x ) { throw new VotorolaRuntimeException( x ); } } /** @param _aCore per {@linkplain #aCore() aCore}. It must already be == * aCore.{@linkplain CoreRevision#contextView(String) contextView}(b). * @param _bCore per {@linkplain #bCore() bCore}. It must already be == * bCore.{@linkplain CoreRevision#contextView(String) contextView}(a). * @param toTestPath whether to test for a PathMismatch. * * @throws IllegalArgumentException if either core's draft is null (incomplete * construction). * @throws PathMismatch if toTestPath is true and either core mismatches. */ private DraftPair( CoreRevision _aCore, CoreRevision _bCore, DiffKeyParse _diffKeyParse, final boolean toTestPath ) throws PathMismatch { init_ensureConstruction( aCore = _aCore ); init_ensureConstruction( bCore = _bCore ); assert aCore == aCore.contextView( bCore.person().username() ); assert bCore == bCore.contextView( aCore.person().username() ); diffKeyParse = _diffKeyParse; if( toTestPath ) { init_testPath( aCore, diffKeyParse.aPath() ); init_testPath( bCore, diffKeyParse.bPath() ); } // Construct mnemonics. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final String aUsername = aCore.person().username(); final String bUsername = bCore.person().username(); final StringBuilder aB = IDPair.buildUserMnemonic( aUsername, new StringBuilder() ); final StringBuilder bB = IDPair.buildUserMnemonic( bUsername, new StringBuilder() ); if( aB.toString().equals( bB.toString() )) // identical mnemonics { final int last = aB.length() - 2; int a = last; int b = last; char aChar = aB.charAt( a ); char bChar = aChar; for( ;; ) // find first aChar/bChar that differ { a = init_toNextLetterOrDigit( aUsername, a ); if( a != 0 ) aChar = aUsername.charAt( a ); b = init_toNextLetterOrDigit( bUsername, b ); if( b != 0 ) bChar = bUsername.charAt( b ); if( aChar != bChar ) { aB.setCharAt( 1, aChar ); bB.setCharAt( 1, bChar ); if( last == 0 ) // we overwrote the trailing '.' { aB.append( '.' ); bB.append( '.' ); } break; } else if( a == 0 && b == 0 ) { aB.setCharAt( 0, Character.toLowerCase( aB.charAt( 0 ))); // lower case of first mnemonic break; } } } aUserMnemonic = aB.toString(); bUserMnemonic = bB.toString(); } private static void init_ensureConstruction( final CoreRevision core ) { if( !core.isFullyConstructed() ) { throw new IllegalArgumentException( "not fully constructed: " + core ); } if( core.draft() == null ) { throw new IllegalArgumentException( new MalformedContent( "cannot construct pairs against core, it does not resolve to a draft", core )); } } private static void init_testPath( final CoreRevision core, final List expectedPath ) throws PathMismatch { final List actual = core.addDraftRevisionPath( new ArrayList( MAX_PATH_LENGTH )); if( !actual.equals( expectedPath )) throw new PathMismatch( core, actual, expectedPath ); } /** @param i the current index, or 0 if none. * @return the next index, or 0 if none. */ private static int init_toNextLetterOrDigit( final String username, int i ) { if( i == 0 ) return i; for( ;; ) { ++i; if( i >= username.length() ) return 0; if( Character.isLetterOrDigit( username.charAt( i ))) return i; } } /** @param _aCore per {@linkplain #aCore() aCore}. It must already be == * aCore.{@linkplain CoreRevision#contextView(String) contextView}(b). * @param _bCore per {@linkplain #bCore() bCore}. It must already be == * bCore.{@linkplain CoreRevision#contextView(String) contextView}(a). */ private DraftPair( CoreRevision _aCore, String _aUserMnemonic, CoreRevision _bCore, String _bUserMnemonic, DiffKeyParse _diffKeyParse ) { aCore = _aCore; bCore = _bCore; assert aCore == aCore.contextView( bCore.person().username() ); assert bCore == bCore.contextView( aCore.person().username() ); aUserMnemonic = _aUserMnemonic; bUserMnemonic = _bUserMnemonic; diffKeyParse = _diffKeyParse; } // ------------------------------------------------------------------------------------ /** The core revision of the a-draft. */ public CoreRevision aCore() { return aCore; } private CoreRevision aCore; /** A short abbreviation of the username of the first draft's author. */ public String aUserMnemonic() { return aUserMnemonic; } private String aUserMnemonic; /** The core revision of the b-draft. */ public CoreRevision bCore() { return bCore; } private CoreRevision bCore; /** A short abbreviation of the username of the second draft's author. */ public String bUserMnemonic() { return bUserMnemonic; } private String bUserMnemonic; /** The difference key of this draft pair. */ public DiffKeyParse diffKeyParse() { return diffKeyParse; } private final DiffKeyParse diffKeyParse; /** Constructs the reverse of this draft pair, in which all components of a-draft and * b-draft are interchanged. */ public DraftPair newReversePair() { return new DraftPair( bCore, bUserMnemonic, aCore, aUserMnemonic, diffKeyParse.newReverseParse() ); } // ==================================================================================== /** Thrown when a request cannot be met because the content of a page is not in the * required form. */ static final @ThreadSafe class MalformedContent extends IOException implements UserInformative { MalformedContent( final String message, final PageRevision page ) { super( message + " | " + MediaWiki.revLoc(page.wikiScriptURI(),page.rev()) ); } } // ==================================================================================== /** Thrown when a request cannot be met because the specifier of a draft pair is not * in the required form. */ static final @ThreadSafe class MalformedSpecifier extends IOException implements UserInformative { MalformedSpecifier( String _message ) { super( _message ); } } // ==================================================================================== /** Thrown when a request cannot be met because an actual draft revision path is * inconsistent with the expected path. */ static final @ThreadSafe class PathMismatch extends IOException implements UserInformative { PathMismatch( final CoreRevision core, final List actualPath, final List expectedPath ) { super( "the specified draft revision path " + expectedPath + " does not match the actual path " + actualPath + " in the wiki | " + MediaWiki.revLoc(core.wikiScriptURI(),core.rev()) ); } PathMismatch( final String message, final CoreRevision core ) { super( message + " | " + MediaWiki.revLoc(core.wikiScriptURI(),core.rev()) ); } } // ==================================================================================== /** Thrown when a request cannot be met because the revision series of the difference * key no longer matches that of the pollwiki. */ static final @ThreadSafe class RevisionSeriesMismatch extends IOException implements UserInformative { RevisionSeriesMismatch( final DiffKeyParse diffKeyParse, final PollwikiVS wiki ) { super( "cannot resolve the specified difference key " + diffKeyParse + " because the pollwiki revision series has since changed from " + diffKeyParse.revisionSeries() + " to " + wiki.revisionSeries() ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static void enlistAnyPointer( final CoreRevision core, final Map listMap ) { // (a) ComponentPipeRevisionL version, cf. (b) ComponentPipeRevision1 version if( core instanceof ComponentPipeRevisionL ) { final ComponentPipeRevisionL component = (ComponentPipeRevisionL)core; enlistAnyPointer_nonComponent( component.candidateVariant(), listMap ); enlistAnyPointer_nonComponent( component.wildVariant(), listMap ); } else enlistAnyPointer_nonComponent( core, listMap ); } private static void enlistAnyPointer_nonComponent( CoreRevision coreOrNull, final Map listMap ) { if( coreOrNull instanceof PointerRevision ) { final PointerRevision pointer = (PointerRevision)coreOrNull; final URI uri = pointer.remoteWikiScriptURI(); PointerList pointerList = listMap.get( uri ); if( pointerList == null ) { pointerList = new PointerList( uri ); listMap.put( uri, pointerList ); } pointerList.add( pointer ); } } private static void enlistAnyPointer( CoreRevision core, final Map listMap, final List revPath ) throws PathMismatch { // (b) ComponentPipeRevision1 version, cf. (a) ComponentPipeRevisionL version final int r; if( core instanceof ComponentPipeRevision1 ) { final ComponentPipeRevision1 component = (ComponentPipeRevision1)core; core = component.variant(); // if any r = 2; } else r = 1; if( core instanceof PointerRevision ) { final PointerRevision pointer = (PointerRevision)core; final URI uri = pointer.remoteWikiScriptURI(); PointerRevList pointerList = listMap.get( uri ); if( pointerList == null ) { pointerList = new PointerRevList( uri ); listMap.put( uri, pointerList ); } pointerList.add( pointer ); ensurePath( core, r, revPath ); pointerList.draftRevList.add( revPath.get( r )); } } private static void enlistIfComponentPipe( final CoreRevision core, final List components ) { if( core instanceof ComponentPipeRevisionL ) components.add( (ComponentPipeRevisionL)core ); } private static void enlistIfComponentPipe( final CoreRevision core, final List components, final List revPath, final List variantRevList ) throws PathMismatch { if( core instanceof ComponentPipeRevision1 ) { components.add( (ComponentPipeRevision1)core ); final int r = 1; ensurePath( core, r, revPath ); variantRevList.add( revPath.get( r )); } } private static void ensurePath( final CoreRevision core, final int r, final List revPath ) throws PathMismatch { if( r >= revPath.size() ) { throw new PathMismatch( "must access revision path index " + r + " in order to resolve draft, but the path you specify is too short: " + revPath, core ); } } private static final Logger logger = LoggerX.i( DraftPair.class ); // ==================================================================================== private static class PointerList extends LinkedList { PointerList( URI _remoteWikiScriptURI ) { remoteWikiScriptURI = _remoteWikiScriptURI; } final URI remoteWikiScriptURI; } // ==================================================================================== private static final class PointerRevList extends PointerList { PointerRevList( URI _remoteWikiScriptURI ) { super( _remoteWikiScriptURI ); } final List draftRevList = new ArrayList<>( /*initial capacity*/2 ); // one draft rev per pointer } }