001package votorola.g; // 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.google.gson.stream.*;
004import java.io.*;
005import java.net.*;
006import java.nio.charset.*;
007import java.util.HashMap;
008import java.util.logging.*; import votorola.g.logging.*;
009import java.util.regex.*;
010import javax.ws.rs.core.UriBuilder;
011import javax.xml.stream.*;
012import votorola.g.hold.*;
013import votorola.g.lang.*;
014import votorola.g.logging.*;
015import votorola.g.net.*;
016import votorola.g.xml.stream.*;
017
018
019/** Utilities for communicating with the MediaWiki wiki system.
020  */
021public @ThreadSafe final class MediaWiki
022{
023
024    private MediaWiki() {}
025
026
027
028    /** The character set for posting to the MediaWiki API.
029      */
030    public static final String API_POST_CHARSET = "UTF-8"; // presumeably
031
032
033
034    /** Appends an already encoded page name to a wiki base URL.  The encoded page name
035      * (e.g. "Main_page") is appended as a simple path (<code>/Main_page</code>) if that
036      * is safe, otherwise as a title query (<code>?title=Main_page</code>).
037      *
038      *     @param b a String builder containing only the wiki base URL, without a
039      *       trailing slash (/).
040      *     @param maybeUgly whether the base URL might be the standard access URL ending
041      *       in "index.php" for example, or is definitely a pretty alias per
042      *       <code>$wgUsePathInfo</code>.
043      *
044      *     @return the same string builder with the page name appended.
045      *
046      *     @see #encodePageSpecifier(UriBuilder,boolean,String)
047      *     @see #MAYBE_UGLY_URL_PATTERN
048      */
049    public static StringBuilder appendPageSpecifier( final StringBuilder b, final boolean maybeUgly,
050      final String encodedPageName )
051    {
052        // We used to simply append the path even to index.php, but that fails with some
053        // wikis.  It fails with Metagov's 1.16 (quite new) wiki for example:
054        // http://metagovernment.org/w/index.php5/Help:Contents
055        //
056        // cf. encodePageSpecifier
057        //
058        // changing?  change also in g/web/gwt/super/
059
060        if( maybeUgly )
061        {
062            b.append( "?title=" );
063            b.append( encodedPageName );
064        }
065        else
066        {
067            b.append( '/' );
068            b.append( encodedPageName );
069        }
070        return b;
071    }
072
073
074
075    /** Completes the URL-decoding of a page name by substituting spaces for underscores.
076      *
077      *     @see <a href='http://www.mediawiki.org/wiki/Help:Magic_words#Page_names' target='_top'
078      *                                                >Help:Magic_words#Page_names</a>
079      */// per INLDOC
080    public static String demiDecodedPageName( final String demiEncodedPageName )
081    {
082        // changing?  change also in g/web/gwt/super/
083        return demiEncodedPageName.replace( '_', ' ' );
084    }
085
086
087
088    /** Partially encodes a page name prior to full URL-encoding, either by substituting
089      * underscores for spaces, or by doing the opposite if the wiki URL is ugly.  In that
090      * case, the page name will be used as a 'title' parameter and the removal of all
091      * demi-encoding is necessary for 'view' actions to be automatically redirected by
092      * the wiki to its pretty alias where one is actually available.  (This despite the
093      * fact that the wiki itself demi-encodes the title parameter for actions such as
094      * 'edit' and 'history'.)
095      *
096      *     @param maybeUgly whether the base URL might be the standard access
097      *       URL ending in "index.php" for example, or is definitely a pretty alias per
098      *       <code>$wgUsePathInfo</code>.
099      *
100      *     @see <a href='http://www.mediawiki.org/wiki/Help:Magic_words#Page_names' target='_top'
101      *                                                >Help:Magic_words#Page_names</a>
102      */// per INLDOC
103    public static String demiEncodedPageName( final String unEncodedPageName,
104      final boolean maybeUgly )
105    {
106        // changing?  change also in g/web/gwt/super/
107        return maybeUgly? demiDecodedPageName( unEncodedPageName ):
108          unEncodedPageName.replace( ' ', '_' );
109    }
110
111
112
113    /** Encodes a page name and appends it to a wiki base URL.  The page name (e.g. "Main
114      * page") is encoded and appended as a simple path (<code>/Main_page</code>) if that
115      * is safe, otherwise as a title query (<code>?title=Main+page</code>).
116      *
117      *     @param ub a URI builder containing only the wiki base URL.
118      *     @param maybeUgly whether the base URL might be the standard access
119      *       URL ending in "index.php" for example, or is definitely a pretty alias per
120      *       <code>$wgUsePathInfo</code>.
121      *
122      *     @return the same URI builder with the page name encoded and appended.
123      *
124      *     @see #appendPageSpecifier(StringBuilder,boolean,String)
125      *     @see #MAYBE_UGLY_URL_PATTERN
126      */
127    public static UriBuilder encodePageSpecifier( final UriBuilder ub, final boolean maybeUgly,
128      String pageName )
129    {
130        // cf. appendPageSpecifier
131
132        pageName = demiEncodedPageName( pageName, maybeUgly ); // will be fully encoded here:
133        if( maybeUgly ) ub.queryParam( "title", pageName );
134        else ub.path( pageName );
135        return ub;
136    }
137
138
139
140    /** Downloads the wikitext source of the specified page into a temporary file.
141      *
142      *     @param idType one of "curid" or "oldid".
143      *     @param id the page identifier (curid) or revision identifier (oldid).
144      *     @param s the base URL for script execution in the wiki, without a trailing
145      *       slash (/).
146      *     @param prefix the {@linkplain File#createTempFile(String,String) prefix}
147      *       for the temporary file.
148      *
149      *     @see <a href='http://www.mediawiki.org/wiki/Manual:Parameters_to_index.php' target='_top'
150      *                         >www.mediawiki.org/wiki/Manual:Parameters_to_index.php</a>
151      */// per INLDOC
152    public static File fetchPageAsFile( final URI s, final String idType, final int id,
153      final String prefix ) throws IOException
154    {
155        final File file = File.createTempFile( prefix, "." + id );
156        final HttpURLConnection http;
157        try
158        {
159            final URI uri = new URI( s + "/index.php?action=raw&" + idType + "=" + id );
160            logger.fine( "querying wiki " + uri );
161            http = (HttpURLConnection)( uri.toURL().openConnection() );
162        }
163        catch( final URISyntaxException x ) { throw new RuntimeException( x ); }
164
165        URLConnectionX.connect( http );
166        try
167        {
168            final BufferedReader in = new BufferedReader( new InputStreamReader(
169              http.getInputStream(), "UTF-8" )); // assuming UTF-8, maybe FIX by reading the HTTP header
170            try
171            {
172                final BufferedWriter out = new BufferedWriter( new OutputStreamWriter(
173                  new FileOutputStream(file), Charset.defaultCharset() )); // to OS charset
174                try
175                {
176                 // ReaderX.appendTo( out, in );
177                 /// or, less efficiently, but giving proper line endings regardless of wiki host:
178                    for( ;; )
179                    {
180                        final String l = in.readLine();
181                        if( l == null ) break;
182
183                        out.write( l );
184                        out.newLine();
185                    }
186                }
187                finally{ out.close(); }
188            }
189            finally{ in.close(); }
190        }
191        finally{ http.disconnect(); }
192
193        return file;
194    }
195
196
197
198    /** Logs into a wiki.
199      *
200      *     @param api the URL of the wiki's <code>api.php</code> script.
201      *
202      *     @return error message for any failure that might be user actionable, such as
203      *       an incorrect password; or null if login succeeds.
204      */
205    public static String login( final URI api, final CookieHandler cookieHandler,
206      final String username, final String password ) throws IOException
207    {
208        final HashMap<String,String> responseMap = new HashMap<String,String>();
209
210      // Request login
211      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
212        final boolean toHandshake;
213        {
214            final HttpURLConnection http = (HttpURLConnection)api.toURL().openConnection();
215            http.setDoOutput( true ); // automatically does setRequestMethod( "POST" )
216            http.setRequestProperty( "Content-Type",
217              "application/x-www-form-urlencoded;charset=" + API_POST_CHARSET );
218            URLConnectionX.setRequestCookies( api, http, cookieHandler ); // after other req headers
219            final Spool spool = new Spool1();
220            try
221            {
222                URLConnectionX.connect( http );
223                spool.add( new Hold() { public void release() { http.disconnect(); }} );
224
225              // write
226              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
227                {
228                    final BufferedWriter out = new BufferedWriter( new OutputStreamWriter(
229                      http.getOutputStream(), API_POST_CHARSET ));
230                    try
231                    {
232                        out.append( "format=xml&action=login&lgname=" );
233                        out.append( URLEncoder.encode( username, API_POST_CHARSET ));
234                        out.append( "&lgpassword=" );
235                        out.append( URLEncoder.encode( password, API_POST_CHARSET ));
236                    }
237                    finally{ out.close(); }
238                }
239
240              // read
241              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
242                cookieHandler.put( api, http.getHeaderFields() );
243                final InputStream in = http.getInputStream();
244                spool.add( new Hold()
245                {
246                    public void release()
247                    {
248                        try{ in.close(); }
249                        catch( final IOException x ) { throw new RuntimeException( x ); }}
250                });
251
252                final XMLStreamReader xml = newXMLStreamReader( in, spool );
253                while( xml.hasNext() )
254                {
255                    xml.next();
256                    if( !xml.isStartElement() ) continue;
257
258                    if( "login".equals( xml.getLocalName() ))
259                    {
260                        for( int a = 0, aN = xml.getAttributeCount(); a < aN; ++a )
261                        {
262                            responseMap.put( /*key*/xml.getAttributeLocalName(a),
263                              xml.getAttributeValue( a ));
264                        }
265                    }
266                    test_error( xml );
267                }
268            }
269            catch( final XMLStreamException x ) { throw new IOException( x ); }
270            finally{ spool.unwind(); }
271            final String result = responseMap.get( "result" );
272            if( "Success".equals( result )) toHandshake = false; // MediaWiki < 1.15.3
273            else if( "NeedToken".equals( result )) toHandshake = true; // >= 1.15.3
274            else return "login call failed with result: " + result;
275        }
276
277      // Handshake to complete login
278      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
279        if( toHandshake )
280        {
281            logger.finer( "handshaking to complete login" );
282            final HttpURLConnection http = (HttpURLConnection)api.toURL().openConnection();
283            http.setDoOutput( true ); // automatically does setRequestMethod( "POST" )
284            http.setRequestProperty( "Content-Type",
285              "application/x-www-form-urlencoded;charset=" + API_POST_CHARSET );
286            URLConnectionX.setRequestCookies( api, http, cookieHandler ); // after other req headers
287            final Spool spool = new Spool1();
288            try
289            {
290                URLConnectionX.connect( http );
291                spool.add( new Hold() { public void release() { http.disconnect(); }} );
292
293              // write
294              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
295                {
296                    final BufferedWriter out = new BufferedWriter(
297                      new OutputStreamWriter( http.getOutputStream(), API_POST_CHARSET ));
298                    try
299                    {
300                        out.append( "format=xml&action=login&lgname=" );
301                        out.append( URLEncoder.encode( username, API_POST_CHARSET ));
302                        out.append( "&lgpassword=" );
303                        out.append( URLEncoder.encode( password, API_POST_CHARSET ));
304                        out.append( "&lgtoken=" ); // echo it back:
305                        out.append( URLEncoder.encode( responseMap.get( "token" ),
306                          API_POST_CHARSET ));
307                    }
308                    finally{ out.close(); }
309                }
310
311              // read
312              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
313                cookieHandler.put( api, http.getHeaderFields() );
314                final InputStream in = http.getInputStream();
315                spool.add( new Hold()
316                {
317                    public void release()
318                    {
319                        try{ in.close(); }
320                        catch( final IOException x ) { throw new RuntimeException( x ); }}
321                });
322
323                final XMLStreamReader xml = newXMLStreamReader( in, spool );
324                responseMap.clear(); // clear previous response
325                while( xml.hasNext() )
326                {
327                    xml.next();
328                    if( !xml.isStartElement() ) continue;
329
330                    if( "login".equals( xml.getLocalName() ))
331                    {
332                        for( int a = 0, aN = xml.getAttributeCount(); a < aN; ++a )
333                        {
334                            responseMap.put( /*key*/xml.getAttributeLocalName(a),
335                              xml.getAttributeValue( a ));
336                        }
337                    }
338                    test_error( xml );
339                }
340            }
341            catch( final XMLStreamException x ) { throw new IOException( x ); }
342            finally{ spool.unwind(); }
343            final String result = responseMap.get( "result" );
344            if( !"Success".equals( result )) return "handshake failed with result: " + result;
345        }
346
347        return null;
348    }
349
350
351
352    /** A pattern that detects whether a wiki URL might be based on the standard access
353      * URL ending in "index.php" for example, or is definitely based on a pretty alias
354      * per <code>$wgUsePathInfo</code>.
355      */
356    public static final Pattern MAYBE_UGLY_URL_PATTERN = Pattern.compile(
357      ".+/index\\.php[0-9]*.*" ); // OK to err on side of false inclusion
358      // changing?  change also in s.gwt.web.PollwikiG
359
360
361
362    /** Constructs a new stream reader suitable for reading a MediaWiki API response, and
363      * reads just to the 'api' element.
364      *
365      *     @param spool an optional spool for the release of associated holds.  When
366      *       unwound it releases the holds of the reader and thereby disables it.
367      *
368      *     @see #requestXML(URLConnection,Spool)
369      */
370    public static @ThreadSafe XMLStreamReader newXMLStreamReader( final InputStream in,
371      final Spool spool ) throws IOException, XMLStreamException
372    {
373        final XMLStreamReader xml;
374        synchronized( XMLInputFactoryX.class )
375        {
376            xml = XMLInputFactoryX.SIMPLE_INPUT_FACTORY.createXMLStreamReader( in );
377        }
378        if( spool != null )
379        {
380            spool.add( new Hold()
381            {
382                public void release()
383                {
384                    try{ xml.close(); }
385                    catch( final XMLStreamException x ) { throw new RuntimeException( x ); }
386                }
387            });
388        }
389
390        while( xml.hasNext() )
391        {
392            xml.next();
393            if( xml.isStartElement() )
394            {
395                final String name = xml.getLocalName();
396                if( "api".equals( name )) return xml;
397
398                throw new MalformedResponse( "expected 'api' element, found '" + name + "'" );
399            }
400        }
401        throw new MalformedResponse( "response missing 'api' element" );
402    }
403
404
405
406    /** Translates the username to normal form by shifting the first letter to uppercase
407      * and substituting spaces for underscores.
408      *
409      *     @return the translated name, which may be the same name; or null if the name
410      *       is null.
411      */
412    public static String normalUsername( String name )
413    {
414        // changing?  change also in g/web/gwt/super/
415        if( name != null )
416        {
417            name = name.replace( '_', ' ' );
418            final char ch = name.charAt( 0 );
419            if( Character.isLowerCase( ch )) name = Character.toUpperCase(ch) + name.substring(1);
420        }
421        return name;
422    }
423
424
425
426    /** Parses a page name (example "Ns:Root/sub/path") into two groups: (1) namespace
427      * "Ns", and (2) local name "Root/sub/path".  The namespace group will be null if not
428      * present.  Note that group values may have underscores from {@linkplain
429      * #demiEncodedPageName(String,boolean) demi-encoding}, depending on where the
430      * provided name string was constructed.
431      *
432      *     @see <a href='http://www.mediawiki.org/wiki/Help:Magic_word#Page_names' target='_top'
433      *                                                >Help:Magic_word#Page_names</a>
434      */// per INLDOC
435    public static MatchResult parsePageName( final String pageName )
436    {
437        final Matcher m = PAGE_NAME_PATTERN.matcher( pageName );
438        return m.matches()? m: null;
439    }
440
441
442        private static final Pattern PAGE_NAME_PATTERN = Pattern.compile( "(?:(.+?):)?(.+)" );
443                                                                            // NS  :   LOCAL
444
445
446    /** Parses a page name (example "Ns:Root/sub/path") into three groups: (1) namespace
447      * "Ns", (2) local root name "Root" and (3) subpage path "sub/path".  The namespace
448      * group will be null if not present, likewise for the subpage path.  Note that group
449      * values may have underscores from {@linkplain #demiEncodedPageName(String,boolean)
450      * demi-encoding}, depending on where the provided name string was constructed.
451      *
452      *     @see <a href='http://www.mediawiki.org/wiki/Help:Magic_word#Page_names' target='_top'
453      *                                                >Help:Magic_word#Page_names</a>
454      */// per INLDOC
455    public static MatchResult parsePageNameS( final String pageName )
456    {
457        final Matcher m = PAGE_NAME_S_PATTERN.matcher( pageName );
458        return m.matches()? m: null;
459    }
460
461
462        private static final Pattern PAGE_NAME_S_PATTERN = Pattern.compile(
463          "(?:(.+?):)?([^/]+)(?:/(.*))?" ); // changing?  change also in g/web/gwt/super/
464          //   NS  :   ROOT     / SUB
465
466
467
468    /** Establishes an HTTP connection and returns a JSON reader for the response.
469      *
470      *     @param _http the connector, which must be of type HttpURLConnection.  The base
471      *       class is accepted only as a convenience to save clients having to cast the
472      *       result of URL.openConnection().
473      *     @param spool a spool for the release of associated holds.  When unwound it
474      *       releases the holds of the reader and thereby disables it.
475      */
476    public static JsonReader requestJSON( URLConnection _http, final Spool spool ) throws IOException
477    {
478        final HttpURLConnection http = (HttpURLConnection)_http;
479        URLConnectionX.connect( http );
480        spool.add( new Hold() { public void release() { http.disconnect(); }} );
481        final JsonReader in = new JsonReader( new BufferedReader( new InputStreamReader(
482          http.getInputStream(), "UTF-8" )));
483        spool.add( new Hold()
484        {
485            public void release()
486            {
487                try{ in.close(); }
488                catch( IOException x ) { throw new RuntimeException( x ); }
489            }
490        });
491        return in;
492    }
493
494
495
496    /** Establishes an HTTP connection and returns an XML reader pre-situated on the 'api'
497      * element of the response body.
498      *
499      *     @param _http the connector, which must be of type HttpURLConnection.  The base
500      *       class is accepted only as a convenience to save clients having to cast the
501      *       result of URL.openConnection().
502      *     @param spool a spool for the release of associated holds.  When unwound it
503      *       releases the holds of the reader and thereby disables it.
504      */
505    public static XMLStreamReader requestXML( URLConnection _http, final Spool spool )
506      throws IOException,  XMLStreamException
507    {
508        final HttpURLConnection http = (HttpURLConnection)_http;
509        URLConnectionX.connect( http );
510        spool.add( new Hold() { public void release() { http.disconnect(); }} );
511        final InputStream in = http.getInputStream();
512        spool.add( new Hold()
513        {
514            public void release()
515            {
516                try{ in.close(); } // because not closed by closing the XMLStreamReader
517                catch( IOException x ) { throw new RuntimeException( x ); }
518            }
519        });
520        return newXMLStreamReader( in, spool );
521    }
522
523
524
525    /** Constructs the URL for a page revision.
526      *
527      *     @param scriptLoc the base location for script execution in the wiki, without a
528      *       trailing slash (/).
529      */
530    public static String revLoc( final String scriptLoc, final int rev )
531    {
532        return scriptLoc + "/index.php?oldid=" + rev;
533    }
534
535
536
537    /** Constructs the URL to a page revision.
538      *
539      *     @param scriptURI the base location for script execution in the wiki, without a
540      *       trailing slash (/).
541      */
542    public static String revLoc( final URI scriptURI, int _rev )
543    {
544        return revLoc( scriptURI.toASCIIString(), _rev );
545    }
546
547
548
549    /** Tests the current element of an API response and throws NoSuchRev if the element
550      * is named 'badrevids'.  The 'badrevids' element is undocumented in the API
551      * (2010-11).
552      *
553      *     @param r a reader positioned at an element start tag.
554      */
555    public static void test_badrevids( final XMLStreamReader r )
556      throws NoSuchRev,  XMLStreamException
557    {
558        if( !"badrevids".equals( r.getLocalName() )) return;
559
560        final StringBuilder b = new StringBuilder();
561        b.append( "No such page revision(s):" );
562        while( r.hasNext() )
563        {
564            r.next();
565            if( r.isStartElement() && "rev".equals( r.getLocalName() ))
566            {
567                b.append( ' ' );
568                b.append( r.getAttributeValue( /*ns*/null, "revid" ));
569            }
570            else if( r.isEndElement() && "badrevids".equals( r.getLocalName() )) break;
571        }
572        throw new NoSuchRev( b.toString() );
573    }
574
575
576
577    /** Tests the current element of an API response and throws an APIError if the element
578      * is named 'error'.
579      *
580      *     @param r a reader positioned at an element start tag.
581      *
582      *     @see <a href='http://www.mediawiki.org/wiki/API:Errors_and_warnings#Errors'
583      *                                   target='_top'>API:Errors_and_warnings#Errors</a>
584      */// per INLDOC
585    public static void test_error( final XMLStreamReader r ) throws APIError
586    {
587        if( "error".equals( r.getLocalName() )) throw new APIError( r );
588    }
589
590
591
592    /** Tests the 'page' element of an API 'info' query response and throws NoSuchPage if
593      * it encodes a 'missing' attribute.  Works for queries that specify a 'titles'
594      * parameter, but not a 'revids' parameter (MediaWiki 1.15.1).  For 'revids', use
595      * instead {@linkplain #test_badrevids(XMLStreamReader) test_badrevids}.
596      *
597      *     @param r a reader positioned at a 'page' element start tag.
598      *
599      *     @throws NoSuchPage if the response indicates a missing page.
600      *     @throws MalformedRequest if the response indicates an invalid page name.
601      *
602      *     @see <a href='http://www.mediawiki.org/wiki/API:Query#Missing_and_invalid_titles'
603      *                                   target='_top'>API:Query#Missing_and_invalid_titles</a>
604      */// per INLDOC
605    public static void testPage_missing( final XMLStreamReader r ) throws NoSuchPage
606    {
607        if( r.getAttributeValue(/*ns*/null,"invalid") != null )
608        {
609            throw new MalformedRequest( "invalid page name: "
610              + r.getAttributeValue(/*ns*/null,"title") );
611        }
612
613        final String missing = r.getAttributeValue( /*ns*/null, "missing" );
614        if( missing == null ) return;
615
616        final StringBuilder b = new StringBuilder();
617        b.append( "No such page:" );
618        String pageName = null; // till found
619        for( int a = r.getAttributeCount() - 1; a >= 0; --a )
620        {
621            final String name = r.getAttributeLocalName( a );
622            if( "missing".equals( name )) continue;
623
624            final String value = r.getAttributeValue( a );
625            b.append( ' ' );
626            b.append( name );
627            b.append( "=\"" );
628            b.append( value );
629            b.append( '"' );
630            if( "title".equals( name )) pageName = value;
631        }
632        if( pageName == null )
633        {
634            assert false;
635            pageName = "NAMELESS_PAGE";
636        }
637        throw new NoSuchPage( b.toString(), pageName );
638    }
639
640
641
642   // ====================================================================================
643
644
645    /** Thrown when a MediaWiki API call returns an explicit error response.
646      */
647    public static @ThreadSafe final class APIError extends IOException
648    {
649
650        /** @param r the response reader having just read the 'error' start element.
651          */
652        public APIError( final String message, final XMLStreamReader r )
653        {
654            super( (message == null? "": message + " " )
655              + "(" + r.getAttributeValue( /*ns*/null, "code" )
656              + "): " + r.getAttributeValue( /*ns*/null, "info" ));
657        }
658
659
660        /** @param r the response reader having just read the 'error' start element.
661          */
662        public APIError( final XMLStreamReader r ) { this( null, r ); }
663
664    }
665
666
667
668   // ====================================================================================
669
670
671    /** Thrown when an improperly formed API call is detected.
672      */
673    public static @ThreadSafe final class MalformedRequest extends RuntimeException
674    {
675        public MalformedRequest( String _message ) { super( _message ); }
676    }
677
678
679
680   // ====================================================================================
681
682
683    /** Thrown when the response to an API call is improperly formed.
684      */
685    public static @ThreadSafe final class MalformedResponse extends RuntimeException
686    {
687        public MalformedResponse( String _message ) { super( _message ); }
688    }
689
690
691
692   // ====================================================================================
693
694
695    /** Thrown when a request cannot be met because an item does not exist.
696      *
697      *     @see java.util.NoSuchElementException
698      */
699    public static abstract @ThreadSafe class NoSuchItem extends IOException
700    {
701        NoSuchItem( String _message ) { super( _message ); }
702    }
703
704
705
706   // ====================================================================================
707
708
709    /** Thrown when a request cannot be met because a page does not exist.
710      */
711    public static @ThreadSafe final class NoSuchPage extends NoSuchItem implements UserInformative
712    {
713
714        /** @see #pageName()
715          */
716        public NoSuchPage( String _message, String _pageName )
717        {
718            super( _message );
719            pageName = _pageName;
720            if( pageName == null ) throw new NullPointerException(); // fail fast
721        }
722
723
724        /** The full name of the non-existent page including any namespace.  Note that the
725          * page name is not automatically added to the message.
726          */
727        public String pageName() { return pageName; }
728
729
730            private final String pageName;
731
732    }
733
734
735
736   // ====================================================================================
737
738
739    /** Thrown when a request cannot be met because a page revision does not exist.
740      */
741    public static @ThreadSafe final class NoSuchRev extends NoSuchItem implements UserInformative
742    {
743        public NoSuchRev( String _message ) { super( _message ); }
744    }
745
746
747
748//// P r i v a t e ///////////////////////////////////////////////////////////////////////
749
750
751    private static final Logger logger = LoggerX.i( MediaWiki.class );
752
753
754}