001package votorola.s.wic.diff; // Copyright 2010-2013, Michael Allan, Christian Weilbach.  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.net.*;
005import java.nio.charset.*;
006import java.sql.SQLException;
007import java.util.*;
008import java.util.logging.*; import votorola.g.logging.*;
009import java.util.regex.*;
010import javax.script.ScriptException;
011import javax.xml.stream.*;
012import org.apache.wicket.Component;
013import org.apache.wicket.AttributeModifier;
014import org.apache.wicket.RestartResponseException;
015import org.apache.wicket.behavior.AttributeAppender;
016import org.apache.wicket.markup.html.IHeaderResponse;
017import org.apache.wicket.markup.html.WebMarkupContainer;
018import org.apache.wicket.markup.html.basic.Label;
019import org.apache.wicket.markup.html.form.Button;
020import org.apache.wicket.markup.html.form.CheckBox;
021import org.apache.wicket.markup.html.form.StatelessForm;
022import org.apache.wicket.markup.html.link.ExternalLink;
023import org.apache.wicket.markup.html.panel.Fragment;
024import org.apache.wicket.markup.repeater.RepeatingView;
025import org.apache.wicket.model.Model;
026import org.apache.wicket.request.mapper.parameter.PageParameters;
027import votorola.a.*;
028import votorola.a.count.*;
029import votorola.a.diff.*;
030import votorola.a.position.*;
031import votorola.a.web.wic.*;
032import votorola.a.web.wic.authen.*;
033import votorola.g.*;
034import votorola.g.hold.*;
035import votorola.g.io.*;
036import votorola.g.lang.*;
037import votorola.g.locale.*;
038import votorola.g.net.*;
039import votorola.g.web.wic.*;
040import votorola.s.wap.*;
041
042import static votorola.g.MediaWiki.API_POST_CHARSET;
043import static votorola.s.gwt.stage.vote.LightableDifference.REL_CANDIDATE;
044import static votorola.s.gwt.stage.vote.LightableDifference.REL_CO;
045import static votorola.s.gwt.stage.vote.LightableDifference.REL_TIGHT_CYCLE;
046import static votorola.s.gwt.stage.vote.LightableDifference.REL_UNKNOWN;
047import static votorola.s.gwt.stage.vote.LightableDifference.REL_VOTER;
048
049
050/** The Wicket interface of the difference bridge, which includes a view of the difference
051  * between a pair of draft revisions.  Two major views (stage and difference) divide the
052  * overall layout and interconnect via specialized {@linkplain votorola.s.gwt.wic.DIn
053  * overlay graphics} (not shown):<pre>
054  *
055  *    +--------------------------------------+
056  *    |               stage                  |
057  *    +--------------------------------------+
058  *    |                                      |
059  *    |                                      |
060  *    |                                      |
061  *    |                                      |
062  *    |             difference               |
063  *    |                                      |
064  *    |                                      |
065  *    |                                      |
066  *    |                                      |
067  *    |                                      |
068  *    +--------------------------------------+</pre>
069  *
070  * <p>At the top is a Crossforum Theatre {@linkplain votorola.s.gwt.stage.StageV stage
071  * view} implented in GWT.  The bulk of the page is occupied by a static HTML view of a
072  * difference between two drafts, together with controls for selectively patching between
073  * them.  Only drafts published as MediaWiki pages are supported.  The pair is specified
074  * by one or more query parameters which together comprise a "pair specifier".  Several
075  * types of pair specifier are supported:</p>
076  *
077  * <h3>Difference key</h3>
078  *
079  * <p>Here is an example of a request using a difference key:</p>
080  *
081  * <blockquote><code><a href='http://reluk.ca:8080/v/w/D?k=3812.3556-3242.3004!1' target='_top'
082  *                           >http://reluk.ca:8080/v/w/D?k=3812.3556-3242.3004!1</a></code></blockquote>
083  *
084  * <table class='definition' style='margin-left:1em'>
085  *     <tr>
086  *         <th class='key'>Key</th>
087  *         <th>Value</th>
088  *         </tr>
089  *     <tr><td class='key'>k</td>
090  *
091  *         <td>The {@linkplain DiffKey difference key}.</td>
092  *
093  *         </tr>
094  *     </table>
095  *
096  * <h3>Convenience redirects</h3>
097  *
098  * <p>Choose one of the following.  Each redirects to a difference view in normal
099  * voter-candidate order where applicable, or lexical order otherwise.</p>
100  *
101  * <table class='definition' style='margin-left:1em'>
102  *     <tr>
103  *         <th class='key'>Key</th>
104  *         <th>Value</th>
105  *         </tr>
106  *     <tr><td class='key'>alterAuthor<br>poll</td>
107  *
108  *         <td>The name of an author and a poll in the local wiki.  The requester is
109  *         redirected to the difference between the latest revisions of that author's
110  *         draft and the authenticated user's draft.  Parameter 's' is interpreted as
111  *         though the user's revision will come first (a) and the other second (b),
112  *         though the actual placement may be the opposite.</td>
113  *
114  *         </tr>
115  *     <tr><td class='key'>aAuthor<br>bAuthor<br>poll</td>
116  *
117  *         <td>The names of two authors and a poll.  The requester is redirected to the
118  *         difference between the latest revisions of their position drafts.</td>
119  *
120  *         </tr>
121  *     </table>
122  *
123  * <h3>Legacy pair specifier</h3>
124  *
125  * <p>This obsolete, parsed form of a difference key specifier (k) is provided for the
126  * service of old links.  It comprises up to 4 query parameters: 'a', 'aR', 'b', and
127  * 'bR'.  For example: (fails because it cannot accommodate the revision series !1)</p>
128  *
129  * <blockquote><code><a href='http://reluk.ca:8080/v/w/D?a=3812&amp;b=3242&amp;aR=3556&amp;bR=3004' target='_top'
130  *                           >http://reluk.ca:8080/v/w/D?a=3812&amp;b=3242&amp;aR=3556&amp;bR=3004</a></code> (fails)</blockquote>
131  *
132  * <table class='definition' style='margin-left:1em'>
133  *     <tr>
134  *         <th class='key'>Key</th>
135  *         <th>Value</th>
136  *         </tr>
137  *     <tr><td class='key'>a</td>
138  *
139  *         <td>The first component in the revision path of the a-draft.</td>
140  *
141  *         </tr>
142  *     <tr><td class='key'>aR</td>
143  *
144  *         <td>The second component in the revision path of the a-draft, if any.</td>
145  *
146  *         </tr>
147  *     <tr><td class='key'>b</td>
148  *
149  *         <td>The first component in the revision path of the b-draft.</td>
150  *
151  *         </tr>
152  *     <tr><td class='key'>bR</td>
153  *
154  *         <td>The second component in the revision path of the b-draft, if any.</td>
155  *
156  *         </tr>
157  *     </table>
158  *
159  * <h3>Other query parameters</h3>
160  *
161  * <table class='definition' style='margin-left:1em'>
162  *     <tr>
163  *         <th class='key'>Key</th>
164  *         <th>Value</th>
165  *         <th>Default</th>
166  *         </tr>
167  *     <tr><td class='key'>s</td>
168  *
169  *         <td>The selectand specified as diff ordinal 'a' or 'b'.  Use 's' or 's=b' to
170  *         select the second revison of the pair.  The choice affects links and other
171  *         controls associated with the difference view and the stage.</td>
172  *
173  *         <td>'a', or the first revision.</td>
174  *
175  *         </tr>
176  *     </table>
177  *
178  *     @see <a href='http://reluk.ca/w/Category:Draft' target='_top'>Category:Draft</a>
179  *     @see <a href='http://reluk.ca/w/Category:Draft_pointer' target='_top'>Category:Draft pointer</a>
180  *     @see <a href='../../../../../../s/wic/diff/WP_D.html' target='_top'>WP_D.html</a>
181  */
182  @ThreadRestricted("wicket") @org.apache.wicket.devutils.stateless.StatelessComponent
183public final class WP_D extends VPageHTML
184{
185
186    // Planned URL mapping strategy:
187    //
188    //     D       ->  WP_D, lt=1 (these are defaults)
189    //     D?f=a1  ->  WP_D, lt=1
190    //     D?f=a2  ->  WP_D, lt=2
191    //     D?f=b2  ->  WP_DiffBridgeB, lt=2
192    //
193    // Where form parameter (f) is unpacked into page layout and line transformer version.
194    // The URL could be squeezed further at any time by a path mounting, like D/a1, etc.
195
196
197    /** Constructs a WP_D.
198      */
199    public WP_D( final PageParameters pP ) throws IOException, SQLException, XMLStreamException
200    { // bookmarkable page iff constructor public & (default|PageParameter)
201        super( pP );
202     // VOWicket.get().vsRun().voteServer().diffCache().LINE_TRANSFORMER.test(); // TEST
203        final boolean bToSelect;
204        {
205            final String s = PageParametersX.getString( pP, "s" );
206            if( s == null || "a".equals(s) ) bToSelect = false;
207            else if( "".equals(s)  || "b".equals(s) ) bToSelect = true;
208            else
209            {
210                VSession.get().error( "bad value for query parameter 's': " + s );
211                throw new RestartResponseException( new WP_Message() );
212            }
213        }
214
215      // Redirect if requested
216      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
217        final VRequestCycle cycle = VRequestCycle.get();
218        final VOWicket app = VOWicket.get();
219        final VSession.User userOrNull = VSession.get().user();
220        final VoteServer.Run vsRun = app.vsRun();
221        final VoteServer vS = vsRun.voteServer();
222        final PollwikiVS wiki = vS.pollwiki();
223        try
224        {
225          // alterAuthor
226          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
227            final String alterAuthor = stringNonEmpty( pP, "alterAuthor" );
228            if( alterAuthor != null )
229            {
230                pP.remove( "alterAuthor" );
231                final String pollName = stringRequired( pP, "poll" );
232                pP.remove( "poll" );
233                if( userOrNull == null )
234                {
235                    pP.set( "returnClass", getClass().getName() );
236                    throw new RestartResponseException( app.authenticator().newLoginPage( pP ));
237                }
238
239                final String username = userOrNull.username();
240                if( username.equals( alterAuthor ))
241                {
242                    VSession.get().info( "attempt to diff latest of same author: " + alterAuthor );
243                    throw new RestartResponseException( new WP_Message() );
244                }
245
246                final CountSource1 countSource = new CountSource1( vsRun );
247                DraftPair pair = DraftPair.newDraftPair( wiki.positionPageName(alterAuthor,pollName),
248                  wiki.positionPageName(username,pollName), vsRun, countSource );
249                final Count count = countSource.count( pollName );
250                if( count != null )
251                {
252                    final CountNodeW bNode = count.countTablePV().get( userOrNull.email() );
253                    if( bNode != null )
254                    {
255                        if( pair.aCore().person().email().equals( bNode.getCandidateEmail() ))
256                        {
257                            pair = pair.newReversePair();
258                        }
259                    }
260                }
261                if( pair.bCore().person().equals( userOrNull )) // then reverse sense of selectand s
262                {
263                    if( bToSelect ) pP.remove( "s" );
264                    else pP.set( "s", "" );
265                }
266                pP.set( "k", pair.diffKeyParse().key() );
267                throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 );
268            }
269
270          // aAuthor, bAuthor, poll
271          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
272            final String aAuthor = stringNonEmpty( pP, "aAuthor" );
273            if( aAuthor != null )
274            {
275                pP.remove( "aAuthor" );
276                final String bAuthor = stringRequired( pP, "bAuthor" );
277                pP.remove( "bAuthor" );
278                if( aAuthor.equals( bAuthor ))
279                {
280                    VSession.get().info( "attempt to diff latest of same author: " + aAuthor );
281                    throw new RestartResponseException( new WP_Message() );
282                }
283
284                final String pollName = stringRequired( pP, "poll" );
285                pP.remove( "poll" );
286                DraftPair pair = DraftPair.newDraftPair( wiki.positionPageName(aAuthor,pollName),
287                  wiki.positionPageName(bAuthor,pollName), vsRun, /*countSource*/null );
288                if( !pair.aCore().person().username().equals( aAuthor ))
289                {
290                    pair = pair.newReversePair();
291                }
292                pP.set( "k", pair.diffKeyParse().key() );
293                throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 );
294            }
295
296          // ` ` `
297          // voterDraft - removed per note [1]
298        }
299        catch( final IOException x )
300        {
301            if( !(x instanceof UserInformative )) throw x;
302
303            VSession.get().error( ThrowableX.toStringExpanded( x ));
304            throw new RestartResponseException( new WP_Message() );
305        }
306
307      // Query wiki for drafts
308      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
309        {
310            final String k = stringNonEmpty( pP, "k" );
311            final DiffKeyParse kP;
312            try
313            {
314                if( k != null ) kP = new DiffKeyParse( k );
315                else
316                {
317                    kP = new DiffKeyParse( legacyRev(pP,"a"), legacyRevOptional(pP,"aR"),
318                      legacyRev(pP,"b"), legacyRevOptional(pP,"bR") );
319                }
320            }
321            catch( final DiffKeyParse.MalformedKey x )
322            {
323                VSession.get().error( x.toString() );
324                throw new RestartResponseException( new WP_Message() );
325            }
326
327            try{ pair = DraftPair.newDraftPair( kP, wiki ); }
328            catch( final IOException x )
329            {
330                if( !( x instanceof UserInformative )) throw x;
331
332                VSession.get().error( ThrowableX.toStringExpanded( x ));
333                throw new RestartResponseException( new WP_Message() );
334            }
335        }
336
337      // Redirect if pair misordered, per DiffKey normal ordering, R/C/U rules
338      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
339        final CoreRevision aCore = pair.aCore();
340        final CoreRevision bCore = pair.bCore();
341        final String aUsername = aCore.person().username();
342        final String bUsername = bCore.person().username();
343        char rel = REL_UNKNOWN; // style symbol for cast relation of a to b
344        {
345            final DiffKeyParse kP = pair.diffKeyParse();
346            boolean isOrdered = true; // safer assumption to prevent redirect cycle
347            order: if( aUsername.equals( bUsername )) // same author, order by revision path R1
348            {
349                final List<Integer> aPath = kP.aPath();
350                final List<Integer> bPath = kP.bPath();
351                int r = 0;
352                for( ;; )
353                {
354                    final int comparison = aPath.get(r).compareTo( bPath.get(r) );
355                    if( comparison > 0 ) break; // R1
356                    else if( comparison < 0 )
357                    {
358                        isOrdered = false; // R1
359                        break;
360                    }
361
362                    ++r;
363                    if( r == aPath.size() )
364                    {
365                        if( r == bPath.size() )
366                        {
367                            VSession.get().info( "attempt to diff same draft: " + aCore );
368                            throw new RestartResponseException( new WP_Message() );
369                        }
370
371                        isOrdered = false; // R2
372                        break;
373                    }
374
375                    if( r == bPath.size() ) break; // R2
376                }
377            }
378            else // two authors
379            {
380                final String pollName = aCore.pollName();
381                if( pollName.equals( bCore.pollName() )) // same poll
382                {
383                    final Count count;
384                    try{ count = vsRun.scopePoll().ensurePoll(pollName).countToReportT(); }
385                    catch( ScriptException x ) { throw new RuntimeException( x ); }
386
387                    if( count != null )
388                    {
389                        final CountTable.PollView countTablePV = count.countTablePV();
390                        final CountNodeW aNode = countTablePV.get( aCore.person().email() );
391                        if( aNode != null )
392                        {
393                            final CountNodeW bNode = countTablePV.get( bCore.person().email() );
394                            if( bNode != null )
395                            {
396                                final String aCanEmail = aNode.getCandidateEmail();
397                                final String bCanEmail = bNode.getCandidateEmail();
398                                if( bNode.email().equals( aCanEmail ))
399                                {
400                                 // if( aNode.holdVolume() == 0 )
401                                 /// (a) with impersonal nodes, now invalid cast-relation test, so:
402                                    if( !aNode.isBaseCandidate() )
403                                    {
404                                        rel = bToSelect? REL_VOTER: REL_CANDIDATE; // C2
405                                        break order;
406                                    }
407
408                                 // assert bNode.holdVolume() != 0; // but (a), so:
409                                    assert bNode.isBaseCandidate(); // must be co-base
410                                    if( aNode.email().equals( bCanEmail ))
411                                    {
412                                        rel = REL_TIGHT_CYCLE; // C3, U1 or U2
413                                        isOrdered = DiffKey.isDartOrdered( aNode, aUsername, bNode,
414                                          bUsername );
415                                    }
416                                    else rel = REL_CO; // C1
417                                    break order;
418                                }
419
420                                if( aNode.email().equals( bCanEmail ))
421                                {
422                                 // if( bNode.holdVolume() == 0 ) // but (a), so:
423                                    if( !bNode.isBaseCandidate() )
424                                    {
425                                        rel = bToSelect? REL_CANDIDATE: REL_VOTER; // C2
426                                    }
427                                    else rel = REL_CO; // C1 co-base, not tight, caught earlier
428                                    isOrdered = false;
429                                    break order;
430                                }
431
432                                if( aCanEmail != null && aCanEmail.equals(bCanEmail) // co-voter
433                              // || aNode.holdVolume() > 0 && bNode.holdVolume() > 0 )
434                              /// but (a), so:
435                                 || aNode.isBaseCandidate() && bNode.isBaseCandidate() ) // co-base
436                                {
437                                    rel = REL_CO; // C3, U1 or U2
438                                    isOrdered = DiffKey.isDartOrdered( aNode, aUsername, bNode,
439                                      bUsername );
440                                    break order;
441                                }
442                            }
443                        }
444                    }
445                }
446                isOrdered = DiffKey.isLexicallyOrdered( aUsername, bUsername ); // U1 or U2
447            }
448            if( !isOrdered )
449            {
450                pP.set( "k", DiffKey.newReverseKey(kP.key()) );
451                if( bToSelect ) pP.remove( "s" );
452                else pP.set( "s", "" );
453                throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 );
454            }
455        }
456
457      // Crossforum Theatre stage
458      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
459        {
460            final StringBuilder b = WC_Stage.appendLeader( userOrNull, vS, cycle );
461            final String selectand;
462            final CoreRevision anchorCore;
463            final CoreRevision otherCore;
464            if( bToSelect )
465            {
466                selectand = "b";
467                anchorCore = bCore;
468                otherCore = aCore;
469            }
470            else
471            {
472                selectand = "a";
473                anchorCore = aCore;
474                otherCore = bCore;
475            }
476
477          // s_gwt_stage
478          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
479            b.append(
480               "_temp = function()"
481             + "{"
482             +    "var fThis = arguments.callee;"
483             +    "if( fThis.fWrapped ) fThis.fWrapped();" // call admin's own config, if any
484             +    "s_gwt_stage_link_NominalDifferenceTargeter_setEnabled();"
485             +    "s_gwt_stage_vote_CountNodeV_setDeselectionGuard( 'Lax' );"
486                    // Allow click to deselect any node in vote track.  With default actor
487                    // set, 'Default' would be same as 'Lax' but slightly slower.
488             +    "s_gwt_stage_vote_DifferenceLight_setScene( '" );
489            b.append( otherCore.person().username() ).append( "', '" );
490            b.append( rel ).append( "' );"
491             + "};"
492             + "_temp.fWrapped = voGWTConfig.s_gwt_stage;"
493             + "voGWTConfig.s_gwt_stage = _temp;" );
494
495          // s_gwt_stage_Stage_init, per WC_Stage below
496          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
497            b.append(
498               "voGWTConfig.s_gwt_stage_Stage_init = function()"
499             + "{"
500             +    "s_gwt_stage_Stage_setDefaultActorName( '" );
501            b.append( anchorCore.person().username() ).append( "' );"
502             +    "s_gwt_stage_Stage_setDefaultDifference("
503             +    "{"
504             +       "key:'"  ).append( pair.diffKeyParse().key() ).append( "',"
505             +       "selectand:'" ).append( selectand ).append( "'"
506             +    "});"
507             +    "s_gwt_stage_Stage_setDefaultPollName( '" );
508            b.append( anchorCore.pollName() ).append( "' );"
509             + "};" );
510
511          // ` ` `
512            add( new WC_Stage( "stage", "votorola.s.gwt.wic.DIn", b, cycle ));
513        }
514
515      // RENDER VIEW
516      // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
517        final BundleFormatter bunW = cycle.bunW();
518        {
519            CoreRevision d = aCore;
520            if( d.person().equals( userOrNull )) userCoreOrNull = d;
521            else
522            {
523                d = bCore;
524                if( d.person().equals( userOrNull )) userCoreOrNull = d;
525            }
526
527            if( bCore.person().equals( aCore.person() ))
528            {
529                patchBar = bunW.l( "s.wic.diff.WP_D.patchBar.sameUser" );
530            }
531            else if( userOrNull == null ) patchBar = PATCH_BAR_LOGGED_OUT;
532            else if( userCoreOrNull == null ) patchBar = PATCH_BAR_NON_AUTHOR;
533            else if( userCoreOrNull instanceof ComponentPipeRevision )
534            {
535                patchBar = bunW.l( "s.wic.diff.WP_D.patchBar.componentPipe" );
536            }
537        }
538        setPageIcon( cycle.staticContextLocation() + "/diff/diff.png" );
539        add( new Label( "title", bunW.l( "s.wic.diff.WP_D.title",
540          pair.aUserMnemonic(), pair.bUserMnemonic() )));
541        add( new WC_NavigationHead( "navHead", WP_D.this, cycle ));
542
543      // Diff
544      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
545        final DiffForm form;
546        final String patchButtonValue = bunW.l( "s.wic.diff.WP_D.patchButtonValue" );
547        final String patchButtonTitle = bunW.l( "s.wic.diff.WP_D.patchButtonTitle" );
548
549        diffFile = vS.diffCache().diffFile( pair );
550        final LineNumberReader in = new LineNumberReader( new InputStreamReader(
551          new FileInputStream(diffFile), Charset.defaultCharset() ));
552        try
553        {
554
555          // Diff header
556          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
557            String line;
558            line = in.readLine();
559            if( line == null || line.length() == 0 ) throw new BadDiffException( "no diff output", diffFile  );
560
561            if( !line.startsWith( "---" ))
562            {
563                final Matcher m = DiffCache.NO_DIFF_PATTERN.matcher( line );
564                if( !m.matches() ) throw new BadDiffException( "diff says '" + line + "'", diffFile );
565
566                VSession.get().info( "no difference between draft revisions " + aCore
567                  + " and " + bCore + " (FIX to handle this more gracefully)" );
568                throw new RestartResponseException( new WP_Message() );
569            }
570
571            {
572                final PageParameters linkP = new PageParameters( pP );
573                final BookmarkablePageLinkX aLink;
574                {
575                    add( new Label( "a-aMnemonic", pair.aUserMnemonic() ));
576                    aLink = new BookmarkablePageLinkX( "a-a", WP_D.class, linkP );
577                    aLink.setBody( aUsername );
578                    add( aLink );
579                }
580                line = in.readLine();
581                if( !line.startsWith( "+++" )) throw new BadDiffException( "missing '+++' line in diff header", diffFile  );
582
583                final BookmarkablePageLinkX bLink;
584                {
585                    add( new Label( "b-aMnemonic", pair.bUserMnemonic() ));
586                    bLink = new BookmarkablePageLinkX( "b-a", WP_D.class,   linkP );
587                    bLink.setBody( bUsername );
588                    add( bLink );
589                }
590                final BookmarkablePageLinkX sLink;
591                final BookmarkablePageLinkX tLink; // other one
592                if( bToSelect )
593                {
594                    sLink = bLink;
595                    tLink = aLink;
596                    linkP.remove( "s" );
597                }
598                else
599                {
600                    sLink = aLink;
601                    tLink = bLink;
602                    linkP.set( "s", "" );
603                }
604                appendStyleClass( sLink, "selected" );
605                sLink.setEnabled( false );
606                sLink.add( AttributeModifier.replace( "id", "sLink" )); // for AnchorLine
607                tLink.add( AttributeModifier.replace( "id", "tLink" )); // for AlterLine
608            }
609
610          // Patch controls (top)
611          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
612            form = new DiffForm(); add( form );
613            if( patchBar == null ) appendStyleClass( form, "patchNoBar" );
614
615            addPatchButton( form, "patchControlTop", patchButtonValue, patchButtonTitle, bunW );
616
617          // Hunks
618          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
619            final RepeatingView tbodyRepeating = new RepeatingView( "tbody" );
620            form.add( tbodyRepeating );
621            line = in.readLine();
622            int h = 1;
623            while( line != null ) line = addHunkTo( tbodyRepeating, line, in, diffFile, h++, cycle );
624        }
625        finally{ in.close(); }
626        appendStyleClass( form, "rel-" + rel );
627
628      // Patch controls (bottom)
629      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
630        addPatchButton( form, "patchControlBottom", patchButtonValue, patchButtonTitle, bunW );
631
632      // Feedback messages
633      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
634        add( new WC_Feedback( "feedback" ));
635
636      // = = =
637        setCacheable( true );
638        setCacheDuration( CACHE_DURATION_YEAR );
639    }
640
641
642
643   // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------
644
645
646    public @Override void renderHead( IHeaderResponse r )
647    {
648        super.renderHead( r );
649        if( !getSession().getFeedbackMessages().isEmpty() )
650        {
651            r.renderOnLoadJavaScript( "location.hash = 'feedback'" );
652        }
653    }
654
655
656
657//// P r i v a t e ///////////////////////////////////////////////////////////////////////
658
659
660    /** @param line the header of the hunk.
661      * @param h the hunk number.
662      *
663      * @return the header of the next hunk, or null if there is none.
664      */
665    private String addHunkTo( final RepeatingView tbodyRepeating, String line,
666      final LineNumberReader in, final File diffFile, final int h, final VRequestCycle cycle )
667        throws IOException
668    {
669        if( !line.startsWith( "@@" )) throw new BadDiffException( "bad hunk header '" + line + "'", diffFile );
670
671        final BundleFormatter bunW = cycle.bunW();
672        final Hunk hunk = new Hunk();
673        hunkList.add( hunk );
674
675        final String hunkID = "_" + h;
676        final WebMarkupContainer tbody = new WebMarkupContainer( tbodyRepeating.newChildId() );
677        tbody.add( AttributeModifier.replace( "id", hunkID )); // here rather than in td
678          // else when targeted, browser may scroll horizontally.  FIX, it still scrolls a little.
679        tbodyRepeating.add( tbody );
680
681      // Checkbox clicker
682      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
683        final String checkID = "c" + h;
684        final AttributeModifier checkTitler = AttributeModifier.replace( "title",
685          bunW.l( "s.wic.diff.WP_D.checkTitle" ));
686        {
687            final CheckBox check = new CheckBox( "inputCheck", hunk );
688            tbody.add( check );
689
690            check.add( AttributeModifier.replace( "id", checkID ));
691            if( patchBar == null )
692            {
693                check.add( checkTitler );
694                check.add( new AttributeAppender( "onclick",
695                  new Model<String>( "_a_diff_WP_D.onPatchCheckboxClick( " + h + " )" ), "; " ));
696            }
697            else check.setEnabled( false );
698        }
699        {
700            final WebMarkupContainer td = new WebMarkupContainer( "tdCheck" );
701            tbody.add( td );
702            if( patchBar == null )
703            {
704             // td.add( checkTitler );
705             //// annoying
706                td.add( new AttributeAppender( "onclick",
707                  new Model<String>( "document.getElementById( '" + checkID + "' ).click()" ), "; " ));
708            }
709        }
710
711      // Lines
712      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
713        final RepeatingView trRepeating = new RepeatingView( "tr" );
714        tbody.add( trRepeating );
715
716        final Model<String> aMnemonic = new Model<String>( pair.aUserMnemonic() );
717        final Model<String> bMnemonic = new Model<String>( pair.bUserMnemonic() );
718        int segment = 0; // in-hunk difference, separated by one or more context lines
719        boolean isSegmentIDSet = false;
720        char prefixCharLast = 0; // force refresh
721        String lineABClass = null;
722        String lineUserClass = null;
723        WebMarkupContainer trHunkTailContext = null; // top line of trailing context, if any
724        for( ;; )
725        {
726            line = in.readLine();
727            if( line == null )
728            {
729                hunk.boundaryLine = in.getLineNumber() + 1;
730                break;
731            }
732
733            if( line.startsWith( "@@" ))
734            {
735                hunk.boundaryLine = in.getLineNumber();
736                break;
737            }
738
739            final WebMarkupContainer tr = new WebMarkupContainer( trRepeating.newChildId() );
740            trRepeating.add( tr );
741
742          // th
743          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
744            final char prefixChar = line.charAt( 0 );
745            final Component th;
746            if( prefixChar != prefixCharLast )
747            {
748                if( prefixChar == ' ' ) // context line
749                {
750                    final Fragment y = new Fragment( "th", "segmentLinkFrag", WP_D.this );
751                    appendStyleClass( y, "segment" );
752                    ++segment; // transit to context marks start of segment, or tail of hunk
753                    trHunkTailContext = tr; // till proven otherwise
754
755                    lineABClass = null;
756                    lineUserClass = null;
757                    final String segmentID = setSegmentID( hunkID, segment, tr );
758                    isSegmentIDSet = true;
759                    y.add( new ExternalLink( "link", /*href*/"#" + segmentID,
760                      /*label*/segmentID.substring(1) ));
761                    th = y;
762                }
763                else // difference line
764                {
765                    th = new Label( "th" );
766                    appendStyleClass( th, "mnemonic" );
767                    trHunkTailContext = null; // we cannot be in the tail, yet
768                    if( !isSegmentIDSet ) // hunk lacks leading context, ID not set above
769                    {
770                        ++segment; // all the same, we must be in a new segment
771                        setSegmentID( hunkID, segment, tr ); // set it here on a diff line
772                        isSegmentIDSet = true;
773                    }
774
775                    final Model<String> mnemonic;
776                    final CoreRevision mnemonicCore;
777                    if( prefixChar == '-' )
778                    {
779                        mnemonic = aMnemonic;
780                        mnemonicCore = pair.aCore();
781                        lineABClass = "a";
782                    }
783                    else
784                    {
785                        mnemonic = bMnemonic;
786                        mnemonicCore = pair.bCore();
787                        lineABClass = "b";
788                    }
789                    lineUserClass = userCoreOrNull == mnemonicCore? "u": "o";
790                    th.setDefaultModel( mnemonic );
791                }
792                prefixCharLast = prefixChar;
793            }
794            else th = new WebMarkupContainer( "th" ); // empty
795            tr.add( th );
796
797            if( lineABClass != null ) appendStyleClass( tr, lineABClass );
798            if( lineUserClass != null ) appendStyleClass( tr, lineUserClass );
799
800          // td
801          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
802            String lineText = line.substring( 1 );
803            final boolean isEmpty = "".equals( lineText );
804            if( isEmpty ) lineText = "&nbsp;"; // ensure row is rendered, give it some guts
805
806            final Label td = new Label( "td", lineText );
807            td.setEscapeModelStrings( !isEmpty );
808            tr.add( td );
809        }
810
811        if( trHunkTailContext != null ) appendStyleClass( trHunkTailContext, "hunkTail" );
812
813        return line;
814    }
815
816
817
818    private void addPatchButton( final WebMarkupContainer y, final String id,
819      final String patchButtonValue, final String patchButtonTitle, final BundleFormatter bunW )
820    {
821        final Fragment yy = new Fragment( id, "patchControlFrag", WP_D.this );
822        y.add( yy );
823
824        final Button button = new Button( "button" );
825        button.add( AttributeModifier.replace( "value", patchButtonValue ));
826        button.add( AttributeModifier.replace( "title", patchButtonTitle ));
827        yy.add( button );
828
829        final Component loginLink;
830        if( patchBar == null ) loginLink = newNullComponent( "loginLink" );
831        else
832        {
833            button.setEnabled( false );
834            if( patchBar.equals( PATCH_BAR_LOGGED_OUT ))
835            {
836                loginLink = new WC_LoginLink( "loginLink", WP_D.this,
837                  bunW.l( "s.wic.diff.WP_D.login" ));
838            }
839            else if( patchBar.equals( PATCH_BAR_NON_AUTHOR )) // typical case, make no noise
840            {
841                loginLink = newNullComponent( "loginLink" );
842            }
843            else // rare edge case, just render it as a disabled link for now
844            {
845                loginLink = new ExternalLink( "loginLink", ".", patchBar );
846                loginLink.setEnabled( false );
847            }
848        }
849        yy.add( loginLink );
850    }
851
852
853
854    private static void appendIfFile( final File fileOrNull, final Charset charset,
855      final Writer out ) throws IOException
856    {
857        if( fileOrNull != null ) FileX.appendTo( out, fileOrNull, charset );
858    }
859
860
861
862    private static void appendIfFile( final File fileOrNull, final Charset charset,
863      final Writer out, final LineTransformer1 transformer ) throws IOException
864    {
865        if( fileOrNull == null ) return;
866
867        final BufferedReader in = new BufferedReader( new InputStreamReader(
868          new FileInputStream(fileOrNull), charset ));
869        try
870        {
871            for( ;; )
872            {
873                final String l = in.readLine();
874                if( l == null ) break;
875
876                transformer.appendToWiki( l, out );
877            }
878        }
879        finally{ in.close(); }
880    }
881
882
883
884    private final File diffFile;
885
886
887
888    private static final String FAILURE_PAGE_TITLE = "Unable to patch";
889      // failure messages not currently localized
890
891
892
893 // /** The pattern of a hunk header.
894 //   */
895 // private static final Pattern HUNK_HEADER_PATTERN = Pattern.compile(
896 //   "^@@ -\\d+,\\d+ \\+\\d+,\\d+ @@$" );
897 //  // @@ -11,6 +12,7 @@
898
899
900
901    private ArrayList<Hunk> hunkList = new ArrayList<Hunk>();
902
903
904
905    private static int legacyRev( final PageParameters pP, final String key )
906    {
907        final int rev = legacyRevOptional( pP, key );
908        if( rev < 0 )
909        {
910            VSession.get().error( "missing query parameter 'k'" );
911            throw new RestartResponseException( new WP_Message() );
912        }
913
914        return rev;
915    }
916
917
918
919    private static int legacyRevOptional( final PageParameters pP, final String keyR )
920    {
921        final String revString = stringNonEmpty( pP, keyR );
922        if( revString == null ) return -1;
923
924        final int rev = Integer.parseInt( revString );
925        if( rev < 0 ) throw new IllegalArgumentException();
926
927        return rev;
928    }
929
930
931
932    private static final Logger logger = LoggerX.i( WP_D.class );
933
934
935
936    private DraftPair pair;
937
938
939
940    /** If non-null, patching is disallowed.
941      */
942    private String patchBar;
943
944
945
946    private static final String PATCH_BAR_LOGGED_OUT = "Login required";
947
948    private static final String PATCH_BAR_NON_AUTHOR = "Patching others' drafts not allowed";
949
950
951
952    private static String setSegmentID( final String hunkID, final int segment,
953      final WebMarkupContainer tr )
954    {
955        final String segmentID = hunkID + "." + segment;
956        tr.add( AttributeModifier.replace( "id", segmentID ));
957        return segmentID;
958    }
959
960
961
962    /** Either pair.aCore or pair.bCore, whichever is the user's.
963      */
964    private CoreRevision userCoreOrNull;
965
966
967
968   // ====================================================================================
969
970
971    private static final class BadDiffException extends RuntimeException
972    {
973        BadDiffException( final String message, final File diffFile )
974        {
975            super( message + ": " + diffFile );
976        }
977    }
978
979
980
981   // ====================================================================================
982
983
984    private final class DiffForm extends StatelessForm<Void>
985    {
986
987        private DiffForm() { super( "form" ); }
988
989
990        protected @Override void onSubmit()
991        {
992            super.onSubmit();
993            if( patchBar != null ) throw new IllegalStateException(); // probably impossible
994
995            final VRequestCycle cycle = VRequestCycle.get();
996
997          // Ensure at least one hunk is included
998          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
999            int hunkCount = 0;
1000            for( final Hunk hunk: hunkList ) if( hunk.getObject() ) ++hunkCount;
1001            if( hunkCount == 0 )
1002            {
1003                VSession.get().getFeedbackMessages().warn( /*reporter*/DiffForm.this,
1004                  cycle.bunW().l( "s.wic.diff.WP_D.patchFail.noHunk" ));
1005                return;
1006            }
1007
1008            final DraftRevision userDraft = userCoreOrNull.draft();
1009            final URI targetScriptLocation = userDraft.wikiScriptURI();
1010            try
1011            {
1012                final Spool tmpFileSpool = new Spool1();
1013
1014              // Construct the patch file from the selected hunks
1015              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1016                final File patchFile = File.createTempFile( "WP_D_patch", "." + userDraft.pageID() );
1017                tmpFileSpool.add( new FileHold( patchFile ));
1018                  // Create patch as file rather than feeding it in via stdin.  I want to
1019                  // pull the output via stdout and don't want to mess with threads.  For
1020                  // an example of that alternative, see thread 'feeder':
1021                  // http://reluk.ca/var/db/repo/votorola/file/fd139156408c/votorola/a/diff/WP_Diff.java
1022                final Charset nativeCharset = Charset.defaultCharset();
1023                {
1024                    final LineNumberReader in = new LineNumberReader( new InputStreamReader(
1025                      new FileInputStream(diffFile), nativeCharset ));
1026                      // reading from native charset, only for sake of line counting
1027                    try
1028                    {
1029                        final BufferedWriter out = new BufferedWriter( new OutputStreamWriter(
1030                          new FileOutputStream(patchFile), nativeCharset )); // back to native
1031                        try
1032                        {
1033                            boolean toInclude = true; // i.e. include all lines (header) prior to first hunk
1034                            for( int h = -1, boundaryLine = 3;; ) // the first hunk starts at line 3
1035                            {
1036                                final String l = in.readLine();
1037                                if( l == null ) break;
1038
1039                                if( in.getLineNumber() == boundaryLine ) // on a new hunk
1040                                {
1041                                    ++h;
1042                                    final Hunk hunk = hunkList.get( h );
1043                                    toInclude = hunk.getObject();
1044                                    boundaryLine = hunk.boundaryLine;
1045                                }
1046
1047                                if( !toInclude ) continue;
1048
1049                                out.append( l );
1050                                out.newLine();
1051                            }
1052                        }
1053                        finally{ out.close(); }
1054                    }
1055                    finally{ in.close(); }
1056                }
1057
1058              // Fetch the user's draft text as the target file
1059              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1060                final File[] targetFileSplit = DiffCache.LINE_TRANSFORMER.fetchPageAsFile(
1061                  targetScriptLocation, "curid", userDraft.pageID(), "WP_D_target", tmpFileSpool );
1062                final File targetFile = targetFileSplit[1];
1063
1064              // Apply the patch to the target file
1065              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1066                final boolean isReversePatch = userCoreOrNull.equals( pair.bCore() );
1067                final CoreRevision otherCore = // as opposed to userCoreOrNull
1068                  isReversePatch? pair.aCore(): pair.bCore();
1069                final StringBuilder outB = new StringBuilder(); // not currently localized
1070                {
1071                    final ProcessBuilder pB = new ProcessBuilder( "/bin/bash", "-c",
1072                      "patch --force --no-backup-if-mismatch --unified "
1073                      + (isReversePatch? "--reverse ": "")
1074                      + targetFile.getName() + " " + patchFile.getName() + " 2>&1" );
1075                    pB.directory( patchFile.getParentFile() );
1076                    logger.fine( "calling out to OS: " + pB.command() );
1077                    pB.directory( patchFile.getParentFile() );
1078                    final Process p = pB.start();
1079                    outB.append( "PATCHING" ).append( '\n' );
1080                    outB.append( "--------" ).append( '\n' );
1081                    ProcessX.appendTo( outB, p, nativeCharset );
1082                    final int exitValue = ProcessX.waitForWithoutInterrupt( p );
1083                    if( exitValue == 1 ) // some hunks won't apply or merge hit conflicts
1084                    {
1085                        outB.append( '\n' );
1086                        outB.append( "patch attempt failed" ).append( '\n' );
1087                        cycle.setResponsePage( new WP_Message( FAILURE_PAGE_TITLE,
1088                          outB.toString() ).pre());
1089                        return;
1090                    }
1091                    else if( exitValue != 0 ) // == 2 which means severe error
1092                    {
1093                        throw new IOException( "exit value of " + exitValue + " from process: "
1094                          + pB.command() + " : " + outB.toString() );
1095                    }
1096                }
1097
1098              // Login if necessary (currently always)
1099              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1100                final URI api;
1101                try{ api = new URI( userDraft.wikiScriptURI().toASCIIString() + "/api.php" ); }
1102                catch( URISyntaxException x ) { throw new RuntimeException( x ); }
1103
1104                String editToken = null;
1105                token: if( editToken == null )
1106                {
1107                    outB.append( '\n' );
1108                    outB.append( "LOGGING IN" ).append( '\n' );
1109                    outB.append( "----------" ).append( '\n' );
1110                    {
1111                        final String entry = "requesting login to " + api;
1112                        logger.fine( entry );
1113                        outB.append( entry ).append( '\n' );
1114                    }
1115                    final VOWicket app = VOWicket.get();
1116                    final CookieHandler cookieHandler = app.cookieManager();
1117                    final String errorMessage = MediaWiki.login( // FIX by detecting existing login in prior query or cookies
1118                      api, cookieHandler, "Vobot", app.vsRun().voteServer().pollwiki().password() );
1119                    if( errorMessage != null )
1120                    {
1121                        outB.append( '\n' );
1122                        outB.append( errorMessage );
1123                        outB.append( '\n' );
1124                        cycle.setResponsePage( new WP_Message( FAILURE_PAGE_TITLE,
1125                          outB.toString() ).pre());
1126                        return;
1127                    }
1128
1129                  // request edit token
1130                  // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
1131                    final URI queryURI;
1132                    try
1133                    {
1134                        queryURI = new URI( api.toString()
1135                          + "?format=xml&action=query&prop=info&intoken=edit&pageids="
1136                          + userDraft.pageID() );
1137                        logger.fine( "requesting edit token for user's draft: " + queryURI );
1138                    }
1139                    catch( URISyntaxException x ) { throw new RuntimeException( x ); }
1140
1141                    final URLConnection http = queryURI.toURL().openConnection();
1142                    URLConnectionX.setRequestCookies( queryURI, http, cookieHandler ); // after other req headers
1143                    final Spool spool = new Spool1();
1144                    try
1145                    {
1146                        final XMLStreamReader xml = MediaWiki.requestXML( http, spool );
1147                        cookieHandler.put( queryURI, http.getHeaderFields() );
1148                        while( xml.hasNext() )
1149                        {
1150                            xml.next();
1151                            if( !xml.isStartElement() ) continue;
1152
1153                            if( "page".equals( xml.getLocalName() ))
1154                            {
1155                                editToken = xml.getAttributeValue( /*ns*/null, "edittoken" );
1156                                break token;
1157                            }
1158
1159                            MediaWiki.test_badrevids( xml );
1160                            MediaWiki.test_error( xml );
1161                        }
1162                    }
1163                    finally{ spool.unwind(); }
1164                    throw new IllegalStateException();
1165                }
1166
1167              // Post patched file to wiki
1168              // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1169                outB.append( '\n' );
1170                outB.append( "POSTING" ).append( '\n' );
1171                outB.append( "-------" ).append( '\n' );
1172                {
1173                    // We cannot redirect the browser to submit the changes via the wiki
1174                    // interactively, i.e. showing a preview or diff screen in advance of
1175                    // saving the changes.  Changes must be posted and clients cannot be
1176                    // forced to post in response to a redirect.
1177                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3
1178                    {
1179                        final String entry = "posting changes to " + api;
1180                        logger.fine( entry );
1181                        outB.append( entry ).append( '\n' );
1182                    }
1183
1184                    final HttpURLConnection http = (HttpURLConnection)api.toURL().openConnection();
1185                    http.setDoOutput( true ); // automatically does setRequestMethod( "POST" )
1186                 // http.setChunkedStreamingMode( /*chunk length, default*/0 );
1187                 /// fails, giving the API help page (1.16.1)
1188                    http.setRequestProperty( "Content-Type",
1189                      "application/x-www-form-urlencoded;charset=" + API_POST_CHARSET );
1190                    final CookieManager cookieManager = VOWicket.get().cookieManager();
1191                    URLConnectionX.setRequestCookies( api, http, cookieManager ); // after other req headers
1192                    final Spool spool = new Spool1();
1193                    try
1194                    {
1195                        URLConnectionX.connect( http );
1196                        spool.add( new Hold() { public void release() { http.disconnect(); }} );
1197
1198                      // write
1199                      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
1200                        {
1201                            Writer out = new BufferedWriter( new OutputStreamWriter(
1202                              http.getOutputStream(), API_POST_CHARSET ));
1203                            try
1204                            {
1205                                out.append( "format=xml&action=edit&title=" );
1206                                out.append( URLEncoder.encode( userDraft.pageName(),
1207                                  API_POST_CHARSET ));
1208                                // must be specified by title, pageids and revids both
1209                                // give error "title parameter must be set"
1210                                out.append( "&summary=" );
1211                                out.append( URLEncoder.encode( "Patch from " + otherCore.pageName(),
1212                                  API_POST_CHARSET ));
1213                                out.append( "&token=" );
1214                                out.append( URLEncoder.encode( editToken, API_POST_CHARSET ));
1215                                out.append( "&text=" ); // remainder of output is encoded:
1216                                out = new URLEncodedWriter( API_POST_CHARSET, out );
1217
1218                                appendIfFile( /*voHiBrac*/targetFileSplit[0],   nativeCharset, out,
1219                                  DiffCache.LINE_TRANSFORMER );
1220                                appendIfFile( targetFile/*targetFileSplit[1]*/, nativeCharset, out,
1221                                  DiffCache.LINE_TRANSFORMER );
1222                                appendIfFile( /*voLoBrac*/targetFileSplit[2],   nativeCharset, out );
1223                            }
1224                            finally{ out.close(); }
1225                        }
1226
1227                      // read
1228                      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
1229                        cookieManager.put( api, http.getHeaderFields() );
1230                        final InputStream in = http.getInputStream();
1231                        spool.add( new Hold()
1232                        {
1233                            public void release()
1234                            {
1235                                try{ in.close(); }
1236                                catch( IOException x ) { throw new RuntimeException( x ); }
1237                            }
1238                        });
1239
1240                        final XMLStreamReader xml = MediaWiki.newXMLStreamReader( in, spool );
1241                        while( xml.hasNext() )
1242                        {
1243                            xml.next();
1244                            if( !xml.isStartElement() ) continue;
1245
1246                            if( "edit".equals( xml.getLocalName() ))
1247                            {
1248                                final String result = xml.getAttributeValue( /*ns*/null, "result" );
1249                                if( !result.equals( "Success" ))
1250                                {
1251                                    outB.append( '\n' );
1252                                    outB.append( "post attempt failed with result: " );
1253                                    outB.append( result ).append ( '\n' );
1254                                    cycle.setResponsePage( new WP_Message( FAILURE_PAGE_TITLE,
1255                                      outB.toString() ).pre());
1256                                    return;
1257                                }
1258                            }
1259
1260                            MediaWiki.test_error( xml );
1261                        }
1262                    }
1263                    finally{ spool.unwind(); }
1264                }
1265
1266                tmpFileSpool.unwind(); // if no exceptions, otherwise keep files for admin to diagnose
1267            }
1268            catch( IOException|XMLStreamException x ) { throw new RuntimeException( x ); }
1269
1270          // Redirect to wiki history
1271          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1272            try
1273            {
1274                final URI uri = new URI( targetScriptLocation + "/index.php?action=history&curid="
1275                  + userDraft.pageID() );
1276                logger.finest( "redirecting client to " + uri );
1277                throw new RedirectException( uri.toASCIIString(), 303 ); // user takes it from there
1278            }
1279            catch( URISyntaxException x ) { throw new RuntimeException( x ); }
1280        }
1281
1282    }
1283
1284
1285
1286   // ====================================================================================
1287
1288
1289    /** A hunk implemented as an overloaded Wicket model.  The members of the model proper
1290      * answer whether to include the hunk in any patch request, while the other members
1291      * record other properties of the hunk.
1292      */
1293    private static final class Hunk extends Model<Boolean>
1294    {
1295
1296        /** The base-one line number of the succeeding hunk header within the
1297          * <code>diff</code> output.  For the final hunk, this is one plus the final line
1298          * number.
1299          */
1300        int boundaryLine; // final after external init
1301
1302    }
1303
1304
1305}
1306
1307
1308// NOTES
1309//
1310// [1] Convenience redirect 'voterDraft' is yanked as dead code.  If it ever needs
1311//     salvaging, here it is:
1312//
1313//         The name of a position page in the local wiki.  The requester is redirected to
1314//         the difference between the latest revisions of that position's draft and the
1315//         corresponding candidate draft.
1316//
1317//     // voterDraft
1318//     // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
1319//       final String voterDraft = stringNonEmpty( pP, "voterDraft" );
1320//       if( voterDraft != null )
1321//       {
1322//           pP.remove( "voterDraft" );
1323//           final MatchResult m = Position.ensurePageName( voterDraft );
1324//           final String pollName = m.group( 3 );
1325//           final String voterName = m.group( 2 );
1326//           final IDPair voter;
1327//           try{ voter = IDPair.fromUsername( voterName ); }
1328//           catch( AddressException x )
1329//           {
1330//               throw new MediaWiki.IDException( "Not a positional page: '"
1331//                 + voterName + "' is not a mailish username:" + x );
1332//           }
1333//
1334//           final Vote vote;
1335//           try
1336//           {
1337//               vote = new Vote( voter,
1338//                 vsRun.scopePoll().ensurePoll(pollName).voterInputTable() );
1339//           }
1340//           catch( ScriptException x ) { throw new RuntimeException( x ); }
1341//
1342//           final IDPair candidate = vote.getCandidate();
1343//           if( candidate.equals( IDPair.NOBODY ))
1344//           {
1345//               VSession.get().info( "no candidate, " + voterName + " has not voted in '"
1346//                 + pollName + "'" );
1347//               throw new RestartResponseException( new WP_Message() );
1348//           }
1349//
1350//           if( voter.equals( candidate ))
1351//           {
1352//               VSession.get().info( "unable to diff vs. candidate, " + voterName +
1353//                 " is voting for self" );
1354//               throw new RestartResponseException( new WP_Message() );
1355//           }
1356//
1357//           DraftPair pair = DraftPair.newDraftPair( voterDraft,
1358//             wiki.positionPageName(candidate.username(),pollName), vS );
1359//           if( !pair.aCore().person().equals( voter )) pair = pair.newReversePair();
1360//           revPut( pP, "a", pair.a() );
1361//           revPut( pP, "aR", pair.aR() );
1362//           revPut( pP, "b", pair.b() );
1363//           revPut( pP, "bR", pair.bR() );
1364//           throw new RedirectException( cycle.uriFor(WP_D.class,pP).toString(), 303 );
1365//       }