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}