001package votorola.a.position; // Copyright 2010-2013, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import com.sun.jersey.api.uri.UriComponent;
004import java.io.*;
005import java.net.*;
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.count.*;
012import votorola.g.*;
013import votorola.g.hold.*;
014import votorola.g.lang.*;
015
016
017/** A particular revision of the single page at the core of a position.
018  *
019  *     @see <a href='http://reluk.ca/w/Category:Position' target='_top'>Category:Position</a>
020  */
021public interface CoreRevision extends PositionalRevision
022{
023
024
025    /** Finishes the construction of this CoreRevision and sets {@linkplain
026      * #isFullyConstructed() isFullyConstructed} true.
027      *
028      *     @param contextPersonName the name of the person from whose viewpoint the
029      *       revision will be viewed (context person), or null if there is no context
030      *       person.
031      *     @param countSource for possible use during this call.  It is never used if
032      *       there is no context person.
033      *
034      *     @throws IllegalStateException if called a second time.
035      */
036    public @ThreadRestricted("constructor") void init( PollwikiVS wiki, String contextPersonName,
037      CountSource countSource ) throws IOException;
038
039
040
041   // - C o r e - R e v i s i o n --------------------------------------------------------
042
043
044    /** Adds to the list the revision path from this core revision (inclusive) to the
045      * terminal {@linkplain #draft() draft} (inclusive).
046      *
047      *     @return the same list.
048      */
049    public List<Integer> addDraftRevisionPath( List<Integer> path );
050
051
052
053    /** Constructs a view of this core revision suited for viewing in the context of the
054      * specified person, or returns this same (==) core revision if it is already suited.
055      */
056    public CoreRevision contextView( final String contextPersonName );
057
058
059
060    /** The position draft.  This may be null if the revision is incompletely constructed.
061      */
062    public DraftRevision draft();
063
064
065
066    /** Answers whether the construction of this CoreRevision is complete.
067      *
068      *     @see #init(PollwikiVS,String,CountSource)
069      */
070    public boolean isFullyConstructed();
071
072
073
074   // ====================================================================================
075
076
077    /** CoreRevision utilities.
078      */
079    public @ThreadSafe static final class U
080    {
081
082        private U() {}
083
084
085
086        private static final Logger logger = LoggerX.i( CoreRevision.U.class );
087
088
089
090        /** @param b a string builder comprising 'titles=TITLES' or 'revids=REVS'.
091          */
092        private static List<Throwable> partialFromQuerySpec( final StringBuilder b,
093          final List<CoreRevision> toCores,
094          final Class<? extends ComponentPipeRevision> componentPipeClass, final PollwikiVS wiki,
095          List<Throwable> userXList, final boolean toContinue ) throws IOException
096        {
097            b.insert( 0, "/api.php?format=xml&action=query&" );
098            b.insert( 0, wiki.scriptURI().toASCIIString() );
099            b.append( '&' ).append( read_QUERY );
100            final URL queryURL = new URL( b.toString() );
101            logger.fine( "querying pollwiki for position cores: " + queryURL );
102            final Spool spool = new Spool1();
103            try
104            {
105                final XMLStreamReader xml = MediaWiki.requestXML( queryURL.openConnection(), spool );
106                for( ;; )
107                {
108                    final CoreRevision core;
109                    try{ core = readPartial( xml, wiki, componentPipeClass ); }
110                    catch( final MediaWiki.NoSuchItem x )
111                    {
112                        if( toContinue ) continue;
113                        else throw x;
114                    }
115                    catch( final IOException x )
116                    {
117                        if( userXList == ThrowableX.ENLIST_NONE // == is correct here
118                         || !(x instanceof UserInformative )) throw x;
119
120                        userXList = ThrowableX.listedThrowable( x, userXList );
121                        continue;
122                    }
123                    if( core == null ) break;
124
125                    toCores.add( core );
126                }
127            }
128            catch( final XMLStreamException x ) { throw new IOException( x ); }
129            finally{ spool.unwind(); }
130            return userXList;
131        }
132
133
134
135       // --------------------------------------------------------------------------------
136
137
138        /** Partially constructs the lastest revision of each specified core page and
139          * appends it to the provided list.  Constructs LocalDraftRevisions fully and
140          * others partially.  Appends revisions in an unspecified order that might not
141          * correspond to the order of elements in pageNameList.
142          *
143          *     @param toCores the list to which the partially constructed cores are to be
144          *       appended.
145          *     @param componentPipeClass the class of component pipe revisions to
146          *       construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1},
147          *       {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for
148          *       neither.
149          *     @param userXList a pre-constructed list to which any user actionable
150          *       exceptions are to be appended, or null if none was pre-constructed.
151          *       Provide {@linkplain ThrowableX#ENLIST_NONE ENLIST_NONE} to instead force
152          *       the immediate throwing of such exceptions.
153          *     @param toContinue whether to continue in the event of a missing page
154          *       (true), or instead to throw a NoSuchPage exception (false).
155          *
156          *     @return the same userXList if there was one; otherwise null if no
157          *       exceptions were listed; otherwise a newly constructed list.
158          *
159          *     @throws MediaWiki.NoSuchPage if toContinue is false and a page is missing.
160          */
161        public static List<Throwable> partialFromPageNames( final List<? extends CharSequence> pageNameList,
162          final List<CoreRevision> toCores,
163          final Class<? extends ComponentPipeRevision> componentPipeClass, final PollwikiVS wiki,
164          final List<Throwable> userXList, final boolean toContinue ) throws IOException
165        {
166            final int pN = pageNameList.size();
167            if( pN == 0 ) return userXList; // nothing to do
168
169            final StringBuilder b = new StringBuilder();
170            b.append( "titles=" );
171            for( int p = 0;; ) // append "name1|name2|name3|" ...
172            {
173                b.append( UriComponent.encode( pageNameList.get(p).toString(),
174                  UriComponent.Type.QUERY_PARAM ));
175                ++p;
176                if( p == pN ) break;
177
178                b.append( "%7C" ); // vertical bar (|)
179            }
180            return partialFromQuerySpec( b, toCores, componentPipeClass, wiki, userXList,
181             toContinue );
182        }
183
184
185
186        /** Partially constructs the specified core revisions and appends them to the
187          * provided list.  Constructs LocalDraftRevisions fully and others partially.
188          * Appends revisions in an unspecified order that might not correspond to the
189          * order of elements in revList.
190          *
191          *     @param toCores the list to which the partially constructed cores are to be
192          *       appended.
193          *     @param componentPipeClass the class of component pipe revisions to
194          *       construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1},
195          *       {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for
196          *       neither.
197          *     @param userXList a pre-constructed list to which any user actionable
198          *       exceptions are to be appended, or null if none was pre-constructed.
199          *       Provide {@linkplain ThrowableX#ENLIST_NONE ENLIST_NONE} to instead force
200          *       the immediate throwing of such exceptions.
201          *     @param toContinue whether to continue in the event of a non-existent
202          *       revision (true), or instead to throw a NoSuchRev exception (false).
203          *
204          *     @return the same userXList if there was one; otherwise null if no
205          *       exceptions were listed; otherwise a newly constructed list.
206          *
207          *     @throws MediaWiki.NoSuchRev if toContinue is false and a revision does not
208          *       exist.
209          */
210        public static List<Throwable> partialFromRevs( final List<Integer> revList,
211          final List<CoreRevision> toCores,
212          final Class<? extends ComponentPipeRevision> componentPipeClass, final PollwikiVS wiki,
213          final List<Throwable> userXList, final boolean toContinue ) throws IOException
214        {
215            final int rN = revList.size();
216            if( rN == 0 ) return userXList; // nothing to do
217
218            final StringBuilder b = new StringBuilder();
219            b.append( "revids=" );
220            for( int r = 0;; ) // append "rev1|rev2|rev3|" ...
221            {
222                b.append( revList.get(r) );
223                ++r;
224                if( r == rN ) break;
225
226                b.append( "%7C" ); // vertical bar (|)
227            }
228            return partialFromQuerySpec( b, toCores, componentPipeClass, wiki, userXList,
229              toContinue );
230        }
231
232
233
234        /** Attempts to fully construct the {@linkplain #contextView(String) context view}
235          * of a CoreRevision from a page query.
236          *
237          *     @param xml a reader for the response to a page query in which at least one
238          *       'page' element might remain to be read.  The query must cover prop
239          *       'info|revisions' (in order) and rvprop 'content', where the content is
240          *       for section 0 only.  See for example {@linkplain #read_QUERY
241          *       read_QUERY}.
242          *     @param componentPipeClass the class of component pipe revisions to
243          *       construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1},
244          *       {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for
245          *       neither.
246          *     @param contextPersonName the name of the person from whose viewpoint the
247          *       revision will be viewed (context person), or null if there is no context
248          *       person.
249          *     @param countSource for possible use during this call.  It is never used if
250          *       there is no context person.
251          *
252          *     @return the context view of the fully constructed instance, or null if no
253          *       'page' element remains to be read.
254          *
255          *     @throws MediaWiki.NoSuchItem if the page or revision (whichever is
256          *       requested) does not exist.
257          */
258        public static CoreRevision read( final XMLStreamReader xml, final PollwikiVS wiki,
259          final Class<? extends ComponentPipeRevision> componentPipeClass,
260          final String contextPersonName, final CountSource countSource )
261            throws IOException, XMLStreamException
262        {
263            CoreRevision core = readPartial( xml, wiki, componentPipeClass );
264            if( core != null )
265            {
266                core.init( wiki, contextPersonName, countSource );
267                core = core.contextView( contextPersonName );
268            }
269            return core;
270        }
271
272
273
274        /** The encoded form of the minimal query parameters required for the {@linkplain
275          * #read(XMLStreamReader,PollwikiVS,Class,String,CountSource) read} and
276          * {@linkplain #readPartial(XMLStreamReader,PollwikiVS,Class) readPartial}
277          * constructors.
278          */
279        public static final String read_QUERY =
280          "prop=info%7Crevisions&rvprop=content%7Cids&rvsection=0";
281
282
283
284        /** Attempts to partially construct a CoreRevision from a page query.  Yields a
285          * full construction for a LocalDraftRevision, a partial one for any other type,
286          * and nothing at all if the response is exhausted.
287          *
288          *     @param xml a reader for the response to a page query in which at least one
289          *       'page' element might remain to be read.  The query must cover prop
290          *       'info|revisions' (in order) and rvprop 'content', where the content is
291          *       for section 0 only.  See for example {@linkplain #read_QUERY
292          *       read_QUERY}.
293          *     @param componentPipeClass the class of component pipe revisions to
294          *       construct: {@linkplain ComponentPipeRevision1 ComponentPipeRevision1},
295          *       {@linkplain ComponentPipeRevisionL ComponentPipeRevisionL}, or null for
296          *       neither.
297          *
298          *     @return the partially constructed instance, or null if no 'page' element
299          *       remains to be read.
300          *
301          *     @throws MediaWiki.NoSuchItem if the page or revision (whichever is
302          *       requested) does not exist.
303          */
304        public static CoreRevision readPartial( final XMLStreamReader xml,
305          final PollwikiVS wiki, final Class<? extends ComponentPipeRevision> componentPipeClass )
306            throws IOException, XMLStreamException
307        {
308            // <page>
309            //   <categories>
310            //   <rev>
311            //   </page>
312            while( xml.hasNext() )
313            {
314                xml.next();
315                if( !xml.isStartElement() ) continue;
316
317                if( "page".equals( xml.getLocalName() ))
318                {
319                    MediaWiki.testPage_missing( xml ); /* when querying by 'titles'; else
320                      if querying by 'revids' see 'badrevids' below */
321                    final int revLatest = Integer.parseInt( xml.getAttributeValue(
322                      /*ns*/null, "lastrevid" ));
323                    final int pageID = Integer.parseInt( xml.getAttributeValue( /*ns*/null,
324                      "pageid" ));
325                    final String pageName = xml.getAttributeValue( /*ns*/null, "title" );
326                    String content0 = null;
327                    int rev = -1;
328                    page: while( xml.hasNext() )
329                    {
330                        xml.next();
331                        if( xml.isEndElement() && "page".equals( xml.getLocalName() )) break page;
332                          // one page, that's all
333
334                        if( xml.isStartElement() )
335                        {
336                            final String name = xml.getLocalName();
337                            if( "rev".equals( name ))
338                            {
339                                rev = Integer.parseInt( xml.getAttributeValue( /*ns*/null,
340                                  "revid" ));
341                                content0 = xml.getElementText(); // moves state to </rev>
342                             // break page; // last prop requested
343                             /// no actual need to be dependent on request order, break on </page>
344                            }
345                         // else if( "categories".equals( name ))
346                         // {
347                         //     categories: while( xml.hasNext() )
348                         //     {
349                         //         xml.next();
350                         //         if( xml.isEndElement() && "categories".equals( xml.getLocalName() ))
351                         //         {
352                         //             break categories;
353                         //         }
354                         //
355                         //         if( xml.isStartElement() && "cl".equals( xml.getLocalName() ))
356                         //         {
357                         //             if( "Category:Draft pointer".equals( xml.getAttributeValue(
358                         //               /*ns*/null, "title" )))
359                         //             {
360                         //                 isDraftPointer = true;
361                         //                 break categories;
362                         //             }
363                         //         }
364                         //     }
365                         // }
366                         /// attempts to optimize pointer detection by restricting pattern
367                         /// searches (below) to categorized pages, but fails because category
368                         /// is tied to page not rev
369                        }
370                    }
371                    if( content0 == null || rev == -1 )
372                    {
373                        throw new MediaWiki.MalformedResponse( "missing 'rev' element" );
374                    }
375
376                    if( componentPipeClass != null )
377                    {
378                        final MatchResult r = MediaWiki.parsePageNameS( pageName );
379                        if( r == null )
380                        {
381                            throw new MediaWiki.MalformedResponse( "malformed page name:"
382                              + pageName );
383                        }
384
385                        if( /*subpage*/r.group(3) == null )
386                        {
387                            final String username = r.group( 2 );
388                            if( wiki.pipeRecognizer().isPipeName( username ))
389                            {
390                                final Matcher m =
391                                  ComponentPipeRevision.TEMPLATE_CALL_PATTERN.matcher( content0 );
392                                if( m.find() )
393                                {
394                                    if( ComponentPipeRevision1.class.equals( componentPipeClass ))
395                                    {
396                                        return new ComponentPipeRevision1( wiki, pageID, pageName,
397                                          rev, revLatest,
398                                          /*nomineeName*/MediaWiki.normalUsername(m.group(1)),
399                                          username, /*pollName*/m.group(2) );
400                                    }
401
402                                    if( ComponentPipeRevisionL.class.equals( componentPipeClass ))
403                                    {
404                                        return new ComponentPipeRevisionL( wiki, pageID, pageName,
405                                          rev, revLatest,
406                                          /*nomineeName*/MediaWiki.normalUsername(m.group(1)),
407                                          username, /*pollName*/m.group(2) );
408                                    }
409
410                                    throw new IllegalArgumentException( "componentPipeClass" );
411                                }
412                            }
413                        }
414                    }
415
416                    // not a component pipe, maybe a draft pointer:
417                    final String remotePageName;
418                    final String siteName;
419                    pointer:
420                    {
421                        Matcher m;
422                        m = PointerRevision.TEMPLATE_CALL_PATTERN_2.matcher( content0 );
423                        if( m.find() )
424                        {
425                            remotePageName = m.group( 1 );
426                            siteName = m.group( 2 );
427                            break pointer;
428                        }
429
430                        m = PointerRevision.TEMPLATE_CALL_PATTERN_1.matcher( content0 );
431                        if( m.find() )
432                        {
433                            assert "metagovernment.org".equals( m.group( 1 ));
434                            siteName = "Stuff:Metagovernment/wiki";
435                            remotePageName = m.group( 2 );
436                            break pointer;
437                        }
438
439                        m = PointerRevision.EXTERNAL_LINK_PATTERN.matcher( content0 );
440                        if( m.find() )
441                        {
442                            assert "metagovernment.org".equals( m.group( 1 ));
443                            siteName = "Stuff:Metagovernment/wiki";
444                            remotePageName = MediaWiki.demiDecodedPageName( UriComponent.decode(
445                              m.group(2), UriComponent.Type.QUERY_PARAM ));
446                            break pointer;
447                        }
448
449                        // neither a component pipe nor a draft pointer, so:
450                        return new LocalDraftRevision( wiki, pageID, pageName, rev, revLatest,
451                          content0 );
452                    }
453
454                    final PageProperty pDesign = new PageProperty( "Design" );
455                    final PageProperty pScriptURL = new PageProperty( "Script URL" );
456                    final PageProperty pURL = new PageProperty( "URL" );
457                    final PagePropertyReader in = new PagePropertyReader( wiki.cache(),
458                      siteName, pDesign, pScriptURL, pURL );
459                    try
460                    {
461                        in.readAllRequested();
462                        final String design = pDesign.getValue();
463                        if( !"Stuff:MediaWiki".equals( design ))
464                        {
465                            throw new PointerRevision.MalformedContent(
466                              "unsupported drafting design: " + design, wiki, rev );
467                        }
468
469                        final String loc = pURL.getValue();
470                        final String scriptLoc = pScriptURL.getValue();
471                        if( loc == null || scriptLoc == null )
472                        {
473                            throw new PointerRevision.MalformedContent( "drafting site '" + siteName
474                              + "' missing property '" + pScriptURL.name() + "' or '" + pURL.name()
475                              + '\'', wiki, rev );
476                        }
477
478                        return new PointerRevision( wiki, pageID, pageName, rev, revLatest,
479                          remotePageName, new URI(loc), new URI(scriptLoc), content0 );
480                    }
481                    catch( final URISyntaxException x )
482                    {
483                        throw new PointerRevision.MalformedURL( x, wiki, rev );
484                    }
485                    finally { in.close(); }
486                }
487                MediaWiki.test_badrevids( xml ); /* when querying by 'revids'; else if
488                  querying by 'titles' see testPage_missing above */
489                MediaWiki.test_error( xml );
490            }
491            return null;
492        }
493
494
495    }
496
497
498}