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}