001package 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.
002
003import com.sun.jersey.api.uri.*;
004import java.io.*;
005import java.net.*;
006import java.util.*;
007import java.util.logging.*;
008import javax.ws.rs.core.*;
009import javax.xml.stream.*;
010import votorola.a.*;
011import votorola.a.count.*;
012import votorola.a.position.*;
013import votorola.a.voter.*;
014import votorola.g.*;
015import votorola.g.hold.*;
016import votorola.g.lang.*;
017import votorola.g.logging.*;
018
019import static votorola.a.position.DraftRevision.MAX_PATH_LENGTH;
020
021
022/** A pair of draft revisions for differencing.
023  */
024public final @ThreadSafe class DraftPair implements Serializable // where serialized?  WP_D.pair
025{
026
027
028    /** Constructs a DraftPair from a parsed difference key.
029      *
030      *     @see #diffKeyParse()
031      */
032    public static DraftPair newDraftPair( final DiffKeyParse diffKeyParse, final PollwikiVS wiki )
033      throws IOException
034    {
035        if( diffKeyParse.revisionSeries() != wiki.revisionSeries() )
036        {
037            throw new RevisionSeriesMismatch( diffKeyParse, wiki );
038        }
039        final List<Integer> aPath = diffKeyParse.aPath();
040        final List<Integer> bPath = diffKeyParse.bPath();
041        if( aPath.equals(bPath) )
042        {
043            throw new MalformedSpecifier( "identical draft revision paths: " + aPath );
044        }
045
046      // Construct the core revisions.
047      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
048        final List<Integer> revList = new ArrayList<>( /*initial capacity*/2 );
049        final CoreRevision aCore;
050        final CoreRevision bCore;
051        {
052            final int aRev = aPath.get( 0 );
053            final int bRev = bPath.get( 0 );
054            revList.add( aRev );
055            revList.add( bRev );
056            final LinkedList<CoreRevision> cores = new LinkedList<>();
057            CoreRevision.U.partialFromRevs( revList, cores, ComponentPipeRevision1.class, wiki,
058              ThrowableX.ENLIST_NONE, /*toContinue*/false );
059            final CoreRevision core1 = cores.getFirst();
060            final CoreRevision core2 = cores.getLast();
061            // resolve unpredictable order of response by matching response revs against
062            // those in request:
063            if( core1.rev() == aRev )
064            {
065                aCore = core1;
066                bCore = core2;
067            }
068            else
069            {
070                aCore = core2;
071                bCore = core1;
072            }
073        }
074
075      // Initialize any component pipes in the cores by constructing their variants.
076      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
077        revList.clear(); // reuse
078        {
079            final List<ComponentPipeRevision1> components = new LinkedList<>();
080            enlistIfComponentPipe( aCore, components, aPath, /*variant*/revList );
081            enlistIfComponentPipe( bCore, components, bPath, /*variant*/revList );
082            final int rN = revList.size(); // 0..2
083            if( rN > 0 )
084            {
085                // cf. ComponentPipeRevisionL.init2
086
087                final List<CoreRevision> variants = new LinkedList<>();
088                CoreRevision.U.partialFromRevs( revList, variants, /*componentPipeClass*/null, wiki,
089                  ThrowableX.ENLIST_NONE, /*toContinue*/false );
090                for( final CoreRevision variant: variants )
091                {
092                    final int variantRev = variant.rev();
093                    // response order unpredictable, search for corresponding component(s):
094                    for( int r = 0; r < rN; ++r )
095                    {
096                        if( revList.get(r) == variantRev ) components.get(r).init( variant );
097                        // keep searching components, other may have same variant
098                    }
099                }
100            }
101        }
102
103      // Initialize any pointers among the cores and variants by constructing their remote drafts.
104      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
105        final HashMap<URI,PointerRevList> listMap = new HashMap<>( // key remoteWikiScriptURI
106          /*initial capacity*/(int)((2 + 1) / 0.75f) + 1 ); // per expected max remote wikis
107        enlistAnyPointer( aCore, listMap, aPath );
108        enlistAnyPointer( bCore, listMap, bPath );
109        for( final PointerRevList pointerList: listMap.values() ) // each remote wiki
110        {
111            // cf. PointerRevision.initConsume
112
113            final List<Integer> draftRevList = pointerList.draftRevList;
114            final int rN = draftRevList.size(); // 1..2
115            assert rN == pointerList.size(); // one per pointer
116            final URI remoteWikiScriptURI = pointerList.remoteWikiScriptURI;
117            final StringBuilder b = new StringBuilder();
118            b.append( remoteWikiScriptURI );
119            b.append( "/api.php?format=xml&action=query&revids=" );
120            for( int r = 0;; ) // append "rev" or "rev1|rev2"
121            {
122                b.append( draftRevList.get(r) );
123                ++r;
124                if( r == rN ) break;
125
126                b.append( "%7C" ); // vertical bar (|)
127            }
128            b.append( "&prop=info%7Crevisions&rvprop=ids" );
129            final URL queryURL = new URL( b.toString() );
130            logger.fine( "querying remote wiki for drafts: " + queryURL );
131            final Spool spool = new Spool1();
132            try
133            {
134                final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool );
135                for( ;; )
136                {
137                    final PageRevision remotePage = PageRevision1.readPageRevision(
138                      remoteWikiScriptURI, xml, /*toLatest*/false );
139                    if( remotePage == null ) break;
140
141                    final int draftRev = remotePage.rev();
142                    // response order unpredictable, search for corresponding pointer(s):
143                    for( int r = 0; r < rN; ++r )
144                    {
145                        if( draftRevList.get(r) == draftRev )
146                        {
147                            final PointerRevision pointer = pointerList.get( r );
148                            pointer.init( new RemoteDraftRevision( pointer, remotePage.pageID(),
149                              remotePage.pageName(), remotePage.rev(), remotePage.revLatest() ));
150                        }
151                        // keep searching pointers, other may point to same remotePage
152                    }
153                }
154            }
155            catch( final XMLStreamException x ) { throw new IOException( x ); }
156            finally{ spool.unwind(); }
157        }
158
159      // Construct the draft pair.
160      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
161        return new DraftPair(
162          aCore.contextView(bCore.person().username()),
163          bCore.contextView(aCore.person().username()), diffKeyParse, /*toTestPath*/true );
164    }
165
166
167
168    /** Constructs a DraftPair from the latest revisions of the specified position cores.
169      * The order of drafts in the resulting pair (aDraft, bDraft) will be lexical by
170      * author name regardless of the order of actual parameters (pageName1, pageName2).
171      *
172      *     @param countSource for possible use during this call, or null to construct one
173      *       internally.
174      */
175    public static DraftPair newDraftPair( final String pageName1, final String pageName2,
176      final VoteServer.Run vsRun, CountSource countSource ) throws IOException
177    {
178        if( pageName1.equals( pageName2 ))
179        {
180            throw new MalformedSpecifier( "identical core names: " + pageName1 );
181        }
182
183      // Construct the core revisions.
184      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
185        final PollwikiVS wiki = vsRun.voteServer().pollwiki();
186        final CoreRevision aCore;
187        final CoreRevision bCore;
188        {
189            final ArrayList<String> pageNameList = new ArrayList<>( /*initial capacity*/2 );
190            pageNameList.add( pageName1 );
191            pageNameList.add( pageName2 );
192            final LinkedList<CoreRevision> cores = new LinkedList<>();
193            CoreRevision.U.partialFromPageNames( pageNameList, cores, ComponentPipeRevisionL.class, wiki,
194              ThrowableX.ENLIST_NONE, /*toContinue*/false );
195            final CoreRevision core1 = cores.getFirst();
196            final CoreRevision core2 = cores.getLast();
197            // resolve unpredictable order of response by matching response page names
198            // against those in request:
199            if( DiffKey.isLexicallyOrdered( core1.person().username(), core2.person().username() ))
200            {
201                aCore = core1;
202                bCore = core2;
203            }
204            else
205            {
206                aCore = core2;
207                bCore = core1;
208            }
209        }
210
211      // Initialize any component pipes in the cores by constructing their variants.
212      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
213        {
214            final List<ComponentPipeRevisionL> components = new LinkedList<>();
215            enlistIfComponentPipe( aCore, components );
216            enlistIfComponentPipe( bCore, components );
217            int c = components.size();
218            if( c > 0 )
219            {
220                if( countSource == null) countSource = new CountSource1( vsRun );
221                for( ;; ) // each component
222                {
223                    --c;
224                    final ComponentPipeRevisionL co = components.get( c );
225                    co.init1( wiki, aCore.person().username(), countSource );
226                    co.init1( wiki, bCore.person().username(), countSource );
227                    if( c == 0 ) break;
228                }
229                ComponentPipeRevisionL.init2( components, wiki, ThrowableX.ENLIST_NONE,
230                  /*toContinue*/false );
231            }
232        }
233
234      // Initialize any pointers among the cores and variants by constructing their remote drafts.
235      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
236        {
237            final HashMap<URI,PointerList> listMap = new HashMap<>( // key remoteWikiScriptURI
238              /*initial capacity*/(int)((2 + 1) / 0.75f) + 1 ); // per expected max remote wikis
239            enlistAnyPointer( aCore, listMap );
240            enlistAnyPointer( bCore, listMap );
241            for( final PointerList pointerList: listMap.values() ) // each remote wiki
242            {
243                PointerRevision.initConsume( pointerList, wiki, pointerList.remoteWikiScriptURI,
244                  /*toContinue*/false );
245            }
246        }
247
248      // Construct the draft pair.
249      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
250        return newFromUncontextedViews( aCore, bCore, wiki );
251    }
252
253
254
255    /** Constructs a DraftPair for the latest of each of the specified position cores that
256      * resolves to a draft (aPageNameList) and appends it to the provided list of pairs.
257      * Uses the latest of bPageName as the second core in each pair.  Appends the pairs
258      * in an unspecified order that might not correspond to the order of elements in
259      * aPageNameList.
260      *
261      *     @param aPageNameList the name list of first (a) core pages, ideally with spare
262      *       capacity to append one additional name.  Duplicate names will not result in
263      *       duplicate pairs because the MediaWiki API collapses them to one.
264      *     @param countSource for possible use during this call, or null to construct one
265      *       internally.
266      *     @param userXList a pre-constructed list to which any user actionable
267      *       exceptions are to be appended, or null if none was pre-constructed.
268      *
269      *     @return the same userXList if there was one; otherwise null if no
270      *       exceptions were encountered; otherwise a newly constructed list.
271      */
272    public static List<Throwable> newDraftPairs( final ArrayList<String> aPageNameList,
273      final String bPageName, final VoteServer.Run vsRun, CountSource countSource,
274      final List<DraftPair> pairs, List<Throwable> userXList ) throws IOException
275    {
276        final int aN = aPageNameList.size();
277        if( aN == 0 ) return userXList; // nothing to do
278
279      // Construct the core revisions.
280      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
281        final PollwikiVS wiki = vsRun.voteServer().pollwiki();
282        final List<CoreRevision> aCores = new LinkedList<>();
283        aPageNameList.add( bPageName ); // temporarily
284        try
285        {
286            userXList = CoreRevision.U.partialFromPageNames( aPageNameList, aCores,
287              ComponentPipeRevisionL.class, wiki, userXList, /*toContinue*/true );
288        }
289        finally{ aPageNameList.remove( aN ); } // restore caller's list
290        if( aCores.size() < 2 ) return userXList; // too many missing, no pair possible
291
292        final CoreRevision bCore;
293        for( final Iterator<CoreRevision> p = aCores.iterator();; )
294        {
295            final CoreRevision core = p.next();
296            if( bPageName.equals( core.pageName() ))
297            {
298                p.remove();
299                bCore = core;
300                break;
301            }
302
303            if( !p.hasNext() )
304            {
305                return ThrowableX.listedThrowable( new MediaWiki.NoSuchPage(
306                  "cannot construct draft pairs against position core \"" + bPageName
307                  + "\", pollwiki has no such page", bPageName ), userXList );
308            }
309        }
310
311      // Initialize any component pipes among the cores by constructing their variants.
312      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
313        {
314            final List<ComponentPipeRevisionL> components = new LinkedList<>();
315            for( final CoreRevision aCore: aCores ) enlistIfComponentPipe( aCore, components );
316            enlistIfComponentPipe( bCore, components );
317            int c = components.size();
318            if( c > 0 )
319            {
320                if( countSource == null) countSource = new CountSource1( vsRun );
321                for( ;; ) // each component
322                {
323                    --c;
324                    final ComponentPipeRevisionL co = components.get( c );
325                    for( final CoreRevision aCore: aCores )
326                    {
327                        co.init1( wiki, aCore.person().username(), countSource );
328                    }
329                    co.init1( wiki, bCore.person().username(), countSource );
330                    if( c == 0 ) break;
331                }
332                ComponentPipeRevisionL.init2( components, wiki, userXList, /*toContinue*/true );
333            }
334        }
335
336      // Initialize any pointers among the cores and variants by constructing their remote drafts.
337      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
338        {
339            final HashMap<URI,PointerList> listMap = new HashMap<>( // key remoteWikiScriptURI
340              /*initial capacity*/(int)((4 + 1) / 0.75f) + 1 ); // per expected max remote wikis
341            for( final CoreRevision aCore: aCores ) enlistAnyPointer( aCore, listMap );
342            enlistAnyPointer( bCore, listMap );
343            for( final PointerList pointerList: listMap.values() ) // each remote wiki
344            {
345                PointerRevision.initConsume( pointerList, wiki, pointerList.remoteWikiScriptURI,
346                  /*toContinue*/true );
347            }
348        }
349
350      // Construct the draft pairs.
351      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
352        if( bCore.draft() == null )
353        {
354            return ThrowableX.listedThrowable( new MalformedContent(
355              "cannot construct pairs against bCore, it does not resolve to a draft", bCore ),
356              userXList );
357        }
358
359        for( final Iterator<CoreRevision> a = aCores.iterator(); a.hasNext(); )
360        {
361            final CoreRevision aCore = a.next();
362            if( aCore.isFullyConstructed() )
363            {
364                pairs.add( newFromUncontextedViews( aCore, bCore, wiki ));
365            }
366        }
367        return userXList;
368    }
369
370
371
372    /** @param _aCore per {@linkplain #aCore() aCore}.  It need not already be ==
373      *   aCore.{@linkplain CoreRevision#contextView(String) contextView}(b).
374      * @param _bCore per {@linkplain #bCore() bCore}.  It need not already be ==
375      *   bCore.{@linkplain CoreRevision#contextView(String) contextView}(a).
376      */
377    private static DraftPair newFromUncontextedViews( CoreRevision aCore, CoreRevision bCore,
378      final PollwikiVS wiki ) throws DiffKeyParse.MalformedKey
379    {
380        aCore = aCore.contextView( bCore.person().username() );
381        bCore = bCore.contextView( aCore.person().username() );
382        final DiffKeyParse diffKeyParse = new DiffKeyParse(
383          aCore.addDraftRevisionPath( new ArrayList<Integer>(MAX_PATH_LENGTH) ),
384          bCore.addDraftRevisionPath( new ArrayList<Integer>(MAX_PATH_LENGTH) ),
385          wiki.revisionSeries() );
386        try
387        {
388            return new DraftPair( aCore, bCore, diffKeyParse, /*toTestPath*/false );
389        }
390        catch( final PathMismatch x ) { throw new VotorolaRuntimeException( x ); }
391    }
392
393
394
395    /** @param _aCore per {@linkplain #aCore() aCore}.  It must already be ==
396      *   aCore.{@linkplain CoreRevision#contextView(String) contextView}(b).
397      * @param _bCore per {@linkplain #bCore() bCore}.  It must already be ==
398      *   bCore.{@linkplain CoreRevision#contextView(String) contextView}(a).
399      * @param toTestPath whether to test for a PathMismatch.
400      *
401      * @throws IllegalArgumentException if either core's draft is null (incomplete
402      *   construction).
403      * @throws PathMismatch if toTestPath is true and either core mismatches.
404      */
405    private DraftPair( CoreRevision _aCore, CoreRevision _bCore, DiffKeyParse _diffKeyParse,
406      final boolean toTestPath ) throws PathMismatch
407    {
408        init_ensureConstruction( aCore = _aCore );
409        init_ensureConstruction( bCore = _bCore );
410        assert aCore == aCore.contextView( bCore.person().username() );
411        assert bCore == bCore.contextView( aCore.person().username() );
412        diffKeyParse = _diffKeyParse;
413        if( toTestPath )
414        {
415            init_testPath( aCore, diffKeyParse.aPath() );
416            init_testPath( bCore, diffKeyParse.bPath() );
417        }
418
419      // Construct mnemonics.
420      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
421        final String aUsername = aCore.person().username();
422        final String bUsername = bCore.person().username();
423        final StringBuilder aB = IDPair.buildUserMnemonic( aUsername, new StringBuilder() );
424        final StringBuilder bB = IDPair.buildUserMnemonic( bUsername, new StringBuilder() );
425        if( aB.toString().equals( bB.toString() )) // identical mnemonics
426        {
427            final int last = aB.length() - 2;
428            int a = last;
429            int b = last;
430            char aChar = aB.charAt( a );
431            char bChar = aChar;
432            for( ;; ) // find first aChar/bChar that differ
433            {
434                a = init_toNextLetterOrDigit( aUsername, a );
435                if( a != 0 ) aChar = aUsername.charAt( a );
436
437                b = init_toNextLetterOrDigit( bUsername, b );
438                if( b != 0 ) bChar = bUsername.charAt( b );
439
440                if( aChar != bChar )
441                {
442                    aB.setCharAt( 1, aChar );
443                    bB.setCharAt( 1, bChar );
444                    if( last == 0 ) // we overwrote the trailing '.'
445                    {
446                        aB.append( '.' );
447                        bB.append( '.' );
448                    }
449                    break;
450                }
451                else if( a == 0 && b == 0 )
452                {
453                    aB.setCharAt( 0, Character.toLowerCase( aB.charAt( 0 )));
454                      // lower case of first mnemonic
455                    break;
456                }
457            }
458        }
459        aUserMnemonic = aB.toString();
460        bUserMnemonic = bB.toString();
461    }
462
463
464        private static void init_ensureConstruction( final CoreRevision core )
465        {
466            if( !core.isFullyConstructed() )
467            {
468                throw new IllegalArgumentException( "not fully constructed: " + core );
469            }
470
471            if( core.draft() == null )
472            {
473                throw new IllegalArgumentException( new MalformedContent(
474                  "cannot construct pairs against core, it does not resolve to a draft", core ));
475            }
476        }
477
478
479        private static void init_testPath( final CoreRevision core,
480          final List<Integer> expectedPath ) throws PathMismatch
481        {
482            final List<Integer> actual = core.addDraftRevisionPath(
483              new ArrayList<Integer>( MAX_PATH_LENGTH ));
484            if( !actual.equals( expectedPath )) throw new PathMismatch( core, actual, expectedPath );
485        }
486
487
488        /** @param i the current index, or 0 if none.
489          * @return the next index, or 0 if none.
490          */
491        private static int init_toNextLetterOrDigit( final String username, int i )
492        {
493            if( i == 0 ) return i;
494
495            for( ;; )
496            {
497                ++i;
498                if( i >= username.length() ) return 0;
499
500                if( Character.isLetterOrDigit( username.charAt( i ))) return i;
501            }
502        }
503
504
505
506    /** @param _aCore per {@linkplain #aCore() aCore}.  It must already be ==
507      *   aCore.{@linkplain CoreRevision#contextView(String) contextView}(b).
508      * @param _bCore per {@linkplain #bCore() bCore}.  It must already be ==
509      *   bCore.{@linkplain CoreRevision#contextView(String) contextView}(a).
510      */
511    private DraftPair( CoreRevision _aCore, String _aUserMnemonic,
512                       CoreRevision _bCore, String _bUserMnemonic, DiffKeyParse _diffKeyParse )
513    {
514        aCore = _aCore;
515        bCore = _bCore;
516        assert aCore == aCore.contextView( bCore.person().username() );
517        assert bCore == bCore.contextView( aCore.person().username() );
518        aUserMnemonic = _aUserMnemonic;
519        bUserMnemonic = _bUserMnemonic;
520        diffKeyParse = _diffKeyParse;
521    }
522
523
524
525   // ------------------------------------------------------------------------------------
526
527
528    /** The core revision of the a-draft.
529      */
530    public CoreRevision aCore() { return aCore; }
531
532
533        private CoreRevision aCore;
534
535
536
537    /** A short abbreviation of the username of the first draft's author.
538      */
539    public String aUserMnemonic() { return aUserMnemonic; }
540
541
542        private String aUserMnemonic;
543
544
545
546    /** The core revision of the b-draft.
547      */
548    public CoreRevision bCore() { return bCore; }
549
550
551        private CoreRevision bCore;
552
553
554
555    /** A short abbreviation of the username of the second draft's author.
556      */
557    public String bUserMnemonic() { return bUserMnemonic; }
558
559
560        private String bUserMnemonic;
561
562
563
564    /** The difference key of this draft pair.
565      */
566    public DiffKeyParse diffKeyParse() { return diffKeyParse; }
567
568
569        private final DiffKeyParse diffKeyParse;
570
571
572
573    /** Constructs the reverse of this draft pair, in which all components of a-draft and
574      * b-draft are interchanged.
575      */
576    public DraftPair newReversePair()
577    {
578        return new DraftPair( bCore, bUserMnemonic, aCore, aUserMnemonic,
579          diffKeyParse.newReverseParse() );
580    }
581
582
583
584   // ====================================================================================
585
586
587    /** Thrown when a request cannot be met because the content of a page is not in the
588      * required form.
589      */
590    static final @ThreadSafe class MalformedContent extends IOException implements UserInformative
591    {
592        MalformedContent( final String message, final PageRevision page )
593        {
594            super( message + " | " + MediaWiki.revLoc(page.wikiScriptURI(),page.rev()) );
595        }
596    }
597
598
599
600   // ====================================================================================
601
602
603    /** Thrown when a request cannot be met because the specifier of a draft pair is not
604      * in the required form.
605      */
606    static final @ThreadSafe class MalformedSpecifier extends IOException implements UserInformative
607    {
608        MalformedSpecifier( String _message ) { super( _message ); }
609    }
610
611
612
613   // ====================================================================================
614
615
616    /** Thrown when a request cannot be met because an actual draft revision path is
617      * inconsistent with the expected path.
618      */
619    static final @ThreadSafe class PathMismatch extends IOException implements UserInformative
620    {
621
622        PathMismatch( final CoreRevision core, final List<Integer> actualPath,
623          final List<Integer> expectedPath )
624        {
625            super( "the specified draft revision path " + expectedPath
626              + " does not match the actual path " + actualPath + " in the wiki | "
627              + MediaWiki.revLoc(core.wikiScriptURI(),core.rev()) );
628        }
629
630        PathMismatch( final String message, final CoreRevision core )
631        {
632            super( message + " | " + MediaWiki.revLoc(core.wikiScriptURI(),core.rev()) );
633        }
634
635    }
636
637
638
639   // ====================================================================================
640
641
642    /** Thrown when a request cannot be met because the revision series of the difference
643      * key no longer matches that of the pollwiki.
644      */
645    static final @ThreadSafe class RevisionSeriesMismatch extends IOException
646      implements UserInformative
647    {
648        RevisionSeriesMismatch( final DiffKeyParse diffKeyParse, final PollwikiVS wiki )
649        {
650            super( "cannot resolve the specified difference key " + diffKeyParse
651              + " because the pollwiki revision series has since changed from "
652              + diffKeyParse.revisionSeries() + " to " + wiki.revisionSeries() );
653        }
654    }
655
656
657
658//// P r i v a t e ///////////////////////////////////////////////////////////////////////
659
660
661    private static void enlistAnyPointer( final CoreRevision core,
662      final Map<URI,PointerList> listMap )
663    {
664        // (a) ComponentPipeRevisionL version, cf. (b) ComponentPipeRevision1 version
665
666        if( core instanceof ComponentPipeRevisionL )
667        {
668            final ComponentPipeRevisionL component = (ComponentPipeRevisionL)core;
669            enlistAnyPointer_nonComponent( component.candidateVariant(), listMap );
670            enlistAnyPointer_nonComponent( component.wildVariant(), listMap );
671        }
672        else enlistAnyPointer_nonComponent( core, listMap );
673    }
674
675
676        private static void enlistAnyPointer_nonComponent( CoreRevision coreOrNull,
677          final Map<URI,PointerList> listMap )
678        {
679            if( coreOrNull instanceof PointerRevision )
680            {
681                final PointerRevision pointer = (PointerRevision)coreOrNull;
682                final URI uri = pointer.remoteWikiScriptURI();
683                PointerList pointerList = listMap.get( uri );
684                if( pointerList == null )
685                {
686                    pointerList = new PointerList( uri );
687                    listMap.put( uri, pointerList );
688                }
689                pointerList.add( pointer );
690            }
691        }
692
693
694
695    private static void enlistAnyPointer( CoreRevision core, final Map<URI,PointerRevList> listMap,
696      final List<Integer> revPath ) throws PathMismatch
697    {
698        // (b) ComponentPipeRevision1 version, cf. (a) ComponentPipeRevisionL version
699
700        final int r;
701        if( core instanceof ComponentPipeRevision1 )
702        {
703            final ComponentPipeRevision1 component = (ComponentPipeRevision1)core;
704            core = component.variant(); // if any
705            r = 2;
706        }
707        else r = 1;
708        if( core instanceof PointerRevision )
709        {
710            final PointerRevision pointer = (PointerRevision)core;
711            final URI uri = pointer.remoteWikiScriptURI();
712            PointerRevList pointerList = listMap.get( uri );
713            if( pointerList == null )
714            {
715                pointerList = new PointerRevList( uri );
716                listMap.put( uri, pointerList );
717            }
718            pointerList.add( pointer );
719            ensurePath( core, r, revPath );
720            pointerList.draftRevList.add( revPath.get( r ));
721        }
722    }
723
724
725
726    private static void enlistIfComponentPipe( final CoreRevision core,
727      final List<ComponentPipeRevisionL> components )
728    {
729        if( core instanceof ComponentPipeRevisionL ) components.add( (ComponentPipeRevisionL)core );
730    }
731
732
733
734    private static void enlistIfComponentPipe( final CoreRevision core,
735      final List<ComponentPipeRevision1> components, final List<Integer> revPath,
736      final List<Integer> variantRevList ) throws PathMismatch
737    {
738        if( core instanceof ComponentPipeRevision1 )
739        {
740            components.add( (ComponentPipeRevision1)core );
741            final int r = 1;
742            ensurePath( core, r, revPath );
743            variantRevList.add( revPath.get( r ));
744        }
745    }
746
747
748
749    private static void ensurePath( final CoreRevision core, final int r,
750      final List<Integer> revPath ) throws PathMismatch
751    {
752        if( r >= revPath.size() )
753        {
754            throw new PathMismatch( "must access revision path index " + r
755              + " in order to resolve draft, but the path you specify is too short: "
756              + revPath, core );
757        }
758    }
759
760
761
762    private static final Logger logger = LoggerX.i( DraftPair.class );
763
764
765
766   // ====================================================================================
767
768
769    private static class PointerList extends LinkedList<PointerRevision>
770    {
771
772        PointerList( URI _remoteWikiScriptURI ) { remoteWikiScriptURI = _remoteWikiScriptURI; }
773
774
775        final URI remoteWikiScriptURI;
776
777    }
778
779
780
781   // ====================================================================================
782
783
784    private static final class PointerRevList extends PointerList
785    {
786
787        PointerRevList( URI _remoteWikiScriptURI ) { super( _remoteWikiScriptURI ); }
788
789
790        final List<Integer> draftRevList = new ArrayList<>( /*initial capacity*/2 );
791          // one draft rev per pointer
792
793    }
794
795
796}