001package votorola.a.position; // Copyright 2011-2013, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. 002 003import com.sun.jersey.api.uri.UriComponent; 004import java.net.*; 005import java.io.*; 006import java.util.*; 007import java.util.logging.*; import votorola.g.logging.*; 008import java.util.regex.*; 009import javax.xml.stream.*; 010import votorola.a.*; 011import votorola.a.voter.*; 012import votorola.g.*; 013import votorola.g.hold.*; 014import votorola.g.lang.*; 015 016 017/** A particular revision of a draft pointer. 018 * 019 * @see <a href='http://reluk.ca/w/Category:Draft_pointer' target='_top' 020 * >Category:Draft pointer</a> 021 */ 022public final @ThreadSafe class PointerRevision extends PageRevision1 implements CoreRevision 023{ 024 025 026 /** Partically constructs a PointerRevision for {@linkplain #init(RemoteDraftRevision) 027 * init} to finish. 028 * 029 * @see #pageID() 030 * @param _pageName it will be normalized. 031 * @see #rev() 032 * @see #revLatest() 033 * @param _draftName it will be normalized. 034 * @see #remoteWikiURI() 035 * @see #remoteWikiScriptURI() 036 * @param _content0 the wikitext of the revision including at least section 0. 037 */ 038 PointerRevision( final PollwikiVS wiki, int _pageID, String _pageName, int _rev, int _revLatest, 039 String _draftName, URI _remoteWikiURI, URI _remoteWikiScriptURI, String _content0 ) 040 throws PositionIDPair.MalformedPageName, PipeRevision.MalformedContent 041 { 042 super( wiki.scriptURI(), _pageID, _pageName, _rev, _revLatest, wiki.uri().toASCIIString(), 043 wiki.maybeUgly() ); 044 draftName = MediaWiki.demiDecodedPageName( _draftName ); // normalize 045 positionID = PositionIDPair.newID( wiki, _pageName, _rev, _content0 ); 046 remoteWikiScriptURI = _remoteWikiScriptURI; 047 remoteWikiURI = _remoteWikiURI; 048 remoteWikiURI_maybeUgly = MediaWiki.MAYBE_UGLY_URL_PATTERN.matcher( 049 remoteWikiURI.toASCIIString() ).matches(); 050 } 051 052 053 054 /** Finishes the construction of this PointerRevision and sets {@linkplain 055 * #isFullyConstructed() isFullyConstructed} true. 056 * 057 * @see #draft() 058 * 059 * @throws IllegalStateException if called a second time. 060 */ 061 public @ThreadRestricted("constructor") void init( RemoteDraftRevision _draft ) 062 { 063 if( isFullyConstructed ) throw new IllegalStateException(); 064 // per API contract of this method, and init(wiki) 065 066 draft = _draft; 067 // if( draft == null ) throw new NullPointerException(); // fail fast 068 /// done here: 069 if( !draft.pageName().equals( draftName )) 070 { 071 throw new IllegalArgumentException( "expecting draft '" + draftName + "', recieved: " 072 + draft.pageName() ); 073 } 074 075 isFullyConstructed = true; 076 } 077 078 079 080 public @ThreadRestricted("constructor") void init( final PollwikiVS wiki, 081 String _contextPersonName, votorola.a.count.CountSource _countSource ) throws IOException 082 { 083 final LinkedList<PointerRevision> pointers = new LinkedList<>(); 084 // cannot use singleton here, as initConsume modifies it 085 pointers.add( PointerRevision.this ); 086 initConsume( pointers, wiki, remoteWikiScriptURI, /*toContinue*/false ); 087 // calls init(draft) which meets API contract regarding duplicate call 088 } 089 090 091 092 /** Attempts to finish constructing the specifed pointer revisions by constructing 093 * their remote drafts, each to the latest revision. Call once only for each pointer 094 * revision. 095 * 096 * @param pointers the list of pointer revisions from which the fully constructed 097 * ones are to be removed. 098 * @param toContinue whether to continue in the event of a missing draft and to 099 * leave the incompletely constructed pointer in the list with a null 100 * {@linkplain #draft() draft} (true), or to throw a NoSuchPage exception 101 * (false). 102 * 103 * @throws MediaWiki.NoSuchPage if toContinue is false and a remote draft is 104 * missing. 105 */ 106 public static @ThreadRestricted("constructor") void initConsume( 107 final List<PointerRevision> pointers, final PollwikiVS wiki, final URI remoteWikiScriptURI, 108 final boolean toContinue ) throws IOException 109 { 110 // cf. DraftPair.newDraftPair(DiffKeyParse,..) "Initialize any pointers..." 111 112 if( pointers.size() == 0 ) return; // nothing to do 113 114 final StringBuilder b = new StringBuilder(); 115 b.append( remoteWikiScriptURI ); 116 b.append( "/api.php?format=xml&action=query&titles=" ); 117 for( final Iterator<PointerRevision> p = pointers.iterator();; ) 118 { 119 b.append( UriComponent.encode( p.next().draftName(), UriComponent.Type.QUERY_PARAM )); 120 if( !p.hasNext() ) break; 121 122 b.append( "%7C" ); // vertical bar (|) 123 } 124 b.append( "&prop=info" ); 125 final URL queryURL = new URL( b.toString() ); 126 logger.fine( "querying remote wiki for drafts: " + queryURL ); 127 final Spool spool = new Spool1(); 128 try 129 { 130 final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool ); 131 for( ;; ) 132 { 133 final PageRevision remotePage; 134 try 135 { 136 remotePage = PageRevision1.readPageRevision( remoteWikiScriptURI, xml, 137 /*toLatest*/true ); 138 } 139 catch( final MediaWiki.NoSuchPage x ) 140 { 141 if( toContinue ) continue; 142 143 throw newMissingRemoteDraft( x, pointers, wiki ); 144 } 145 146 if( remotePage == null ) break; 147 148 // response order unpredictable, search for corresponding pointer(s): 149 final String remotePageName = remotePage.pageName(); 150 final Iterator<PointerRevision> p = pointers.iterator(); 151 while( p.hasNext() ) 152 { 153 final PointerRevision pointer = p.next(); 154 if( pointer.draftName().equals( remotePageName )) 155 { 156 p.remove(); // speed subsequent searches, meet API contract 157 pointer.init( new RemoteDraftRevision( pointer, remotePage.pageID(), 158 remotePageName, remotePage.rev(), remotePage.revLatest() )); 159 // keep searching pointers, others may point to same remotePage 160 } 161 } 162 } 163 } 164 catch( final XMLStreamException x ) { throw new IOException( x ); } 165 finally{ spool.unwind(); } 166 assert toContinue || pointers.size() == 0; 167 } 168 169 170 171 // ------------------------------------------------------------------------------------ 172 173 174 /** The page name of the remote draft including the namespace. 175 */ 176 public String draftName() { return draftName; } 177 178 179 private final String draftName; 180 181 182 183 /** The search pattern for an external link to a remote draft in an obsolete draft 184 * pointer page. If the pattern matches, it splits the name into groups (1) domain 185 * name (always "metagovernment.org") and (2) page name of remote draft encoded as a 186 * URL parameter value ("User:Joe/my_draft"). 187 * 188 * @see <a href='http://reluk.ca/mediawiki/index.php?title=Category:Draft_pointer&oldid=4015' target='_top' 189 * >Category:Draft_pointer&oldid=4015</a> 190 */ 191 static final Pattern EXTERNAL_LINK_PATTERN = Pattern.compile( 192 "(metagovernment\\.org)/\\S+/index\\.php[0-9]?\\?(?:\\S+&)?title=(\\S+)(?:&|\\s)" ); 193 // SITE DOMAIN REMOTE DRAFT 194 195 196 197 private static final Logger logger = LoggerX.i( PointerRevision.class ); 198 199 200 201 /** The base URI for script execution in the remote wiki, without a trailing slash 202 * (/). 203 * 204 * @see PageRevision#wikiScriptURI() 205 * @see <a href='http://havoc.zelea.com/w/Template:Drafting_site_properties' target='_top' 206 * >Template:Drafting site properties</a> 207 */ 208 public URI remoteWikiScriptURI() { return remoteWikiScriptURI; } 209 210 211 private final URI remoteWikiScriptURI; 212 213 214 215 /** The base URI for requesting pages from the remote wiki, without a trailing slash 216 * (/). This is either the standard access URI ending in something like "index.php" 217 * for example, or an alias for it. 218 * 219 * @see <a href='http://havoc.zelea.com/w/Template:Drafting_site_properties' target='_top' 220 * >Template:Drafting site properties</a> 221 */ 222 public URI remoteWikiURI() { return remoteWikiURI; } 223 224 225 private final URI remoteWikiURI; 226 227 228 229 /** Answers whether the remote wiki URI might be based on the standard access URL 230 * ending in "index.php" for example, or is definitely based on an alias. In the 231 * latter case <code><a href='http://www.mediawiki.org/wiki/Manual:$wgUsePathInfo' 232 * target='_top'>$wgUsePathInfo</a></code> may be assumed true. 233 */ 234 public boolean remoteWikiURI_maybeUgly() { return remoteWikiURI_maybeUgly; } 235 236 237 private final boolean remoteWikiURI_maybeUgly; 238 239 240 241 /** The search pattern for a draft pointer template call in an obsolete draft pointer 242 * page. If the pattern matches, it splits the name into groups based on the 243 * template parameter values (1) domain name (always "metagovernment.org") and (2) 244 * full page name of remote draft (e.g. "User:Joe/my draft"). 245 * 246 * 247 * @see <a href='http://reluk.ca/mediawiki/index.php?title=Category:Draft_pointer&oldid=5351' target='_top' 248 * >Category:Draft_pointer&oldid=5351</a> 249 */ 250 static final Pattern TEMPLATE_CALL_PATTERN_1 = Pattern.compile( "\\{\\s*\\{\\s*draft pointer\\s*" 251 + "\\|\\s*index\\s*=\\s*http://\\S*(metagovernment\\.org)/\\S+/index\\.php[0-9]?\\s*" // SITE DOMAIN 252 + "\\|\\s*page\\s*=\\s*([^=}]+?)\\s*" // REMOTE DRAFT 253 + "\\}\\s*\\}" ); 254 255 256 257 /** The search pattern for a draft pointer template call in a draft pointer page. If 258 * the pattern matches, it splits the name into groups based on the template 259 * parameter values (1) full page name of remote draft (e.g. "User:Joe/my_draft") and 260 * (2) full page name of drafting site ("Stuff:Metagovernment/wiki"). This method of 261 * extracting properties from a revision is required because SemanticMediawiki's data 262 * store does not attach properties to revisions, only to pages. 263 * 264 * @see <a href='http://reluk.ca/w/Category:Draft_pointer' target='_top' 265 * >Category:Draft_pointer</a> 266 */ 267 static final Pattern TEMPLATE_CALL_PATTERN_2 = Pattern.compile( "\\{\\s*\\{\\s*draft pointer\\s*" 268 + "\\|\\s*page\\s*=\\s*([^=}]+?)\\s*" // REMOTE DRAFT 269 + "\\|\\s*site\\s*=\\s*([^=}]+?)\\s*" // SITE PAGE 270 + "\\}\\s*\\}" ); 271 272 273 274 // - C o r e - R e v i s i o n -------------------------------------------------------- 275 276 277 public List<Integer> addDraftRevisionPath( final List<Integer> path ) 278 { 279 path.add( rev() ); 280 path.add( draft.rev() ); 281 return path; 282 } 283 284 285 286 public CoreRevision contextView( String _contextPersonName ) { return PointerRevision.this; } 287 288 289 290 public RemoteDraftRevision draft() { return draft; } 291 292 293 private RemoteDraftRevision draft; // final when initialized 294 295 296 297 /** @see #init(RemoteDraftRevision) 298 */ 299 public boolean isFullyConstructed() { return isFullyConstructed; } 300 301 302 private boolean isFullyConstructed; 303 304 305 306 // - P o s i t i o n a l - R e v i s i o n -------------------------------------------- 307 308 309 public IDPair person() { return positionID.person(); } 310 311 312 313 public String pollName() { return positionID.pollName(); } 314 315 316 317 // ==================================================================================== 318 319 320 /** Thrown when a request cannot be met because the content of a page is not in the 321 * form required for a draft pointer. 322 */ 323 static @ThreadSafe class MalformedContent extends IOException implements UserInformative 324 { 325 326 MalformedContent( final String message, final PollwikiVS wiki, final int rev ) 327 { 328 super( message + " | " + MediaWiki.revLoc(wiki.scriptURI(),rev) ); 329 } 330 331 MalformedContent( final String message, Throwable _cause, final PollwikiVS wiki, 332 final int rev ) 333 { 334 super( message + " | " + MediaWiki.revLoc(wiki.scriptURI(),rev), _cause ); 335 } 336 337 MalformedContent( Throwable _cause, final PollwikiVS wiki, final int rev ) 338 { 339 super( MediaWiki.revLoc(wiki.scriptURI(),rev), _cause ); 340 } 341 342 } 343 344 345 346 // ==================================================================================== 347 348 349 /** Thrown when a request cannot be met because a draft pointer encodes an external 350 * link with a malformed URL. 351 */ 352 static @ThreadSafe final class MalformedURL extends MalformedContent 353 { 354 MalformedURL( Throwable _cause, PollwikiVS _wiki, int _rev ) 355 { 356 super( _cause, _wiki, _rev ); 357 } 358 } 359 360 361 362//// P r i v a t e /////////////////////////////////////////////////////////////////////// 363 364 365 /** Constructs an appropriate exception to throw when a pointer points to a 366 * non-existent remote draft. 367 * 368 * @param x the original exception that signaled the missing draft. 369 * @param pointers a list of pointers including the broken one. 370 */ 371 private static IOException newMissingRemoteDraft( final MediaWiki.NoSuchPage x, 372 final List<PointerRevision> pointers, final PollwikiVS wiki ) 373 { 374 final String remotePageName = x.pageName(); // find corresponding pointer: 375 PointerRevision brokenPointer = null; 376 for( PointerRevision p: pointers ) 377 { 378 if( p.draftName().equals( remotePageName )) 379 { 380 brokenPointer = p; 381 break; 382 } 383 } 384 if( brokenPointer == null ) 385 { 386 assert false; 387 return x; 388 } 389 390 return new MalformedContent( "points to non-existent draft page", x, wiki, 391 brokenPointer.rev() ); 392 } 393 394 395 396 private final PositionIDPair positionID; 397 398 399}