001package votorola.a; // Copyright 2008-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 java.io.*;
004import java.net.*;
005import java.util.logging.*; import votorola.g.logging.*;
006import javax.ws.rs.core.UriBuilder;
007import javax.xml.stream.*;
008import votorola.a.position.*;
009import votorola.g.*;
010import votorola.g.hold.*;
011import votorola.g.lang.*;
012
013
014/** A pollwiki from the viewpoint of a vote-server.
015  *
016  *     @see VoteServer#pollwiki()
017  *     @see <a href='../../../../s/manual.xht#wiki' target='_top'
018  *                           >../s/manual.xht#wiki</a>
019  */
020public @ThreadSafe final class PollwikiVS implements Pollwiki
021{
022
023
024    /** Partially constructs a PollwikiVS for {@linkplain #init(VoteServer) init} to finish.
025      */
026    PollwikiVS( final ConstructionContext cc ) throws URISyntaxException
027    {
028        uri = cc.getURI();
029        password = cc.getPassword();
030        revisionSeries = cc.getRevisionSeries();
031        scriptURI = cc.getScriptURI();
032        maybeUgly = MediaWiki.MAYBE_UGLY_URL_PATTERN.matcher( uri.toASCIIString() ).matches();
033    }
034
035
036
037    /** Completes the construction of this PollwikiVS.  Call once only.
038      */
039    void init( final VoteServer vS ) throws IOException { cache = new WikiCache( vS ); }
040      // after vS.pollwiki assigned
041
042
043
044   // ------------------------------------------------------------------------------------
045
046
047    /** Encodes a page name and appends it to the wiki {@linkplain #uri() base URL}.
048      * Usage example:<pre>
049      *
050      *     String href = voteServer.pollwiki().encodePageSpecifier(
051      *       "Main page" ).build().toASCIIString();</pre>
052      *
053      *     @see #appendPageSpecifier(String)
054      */
055    public UriBuilder encodePageSpecifier( final String pageName )
056    {
057        return MediaWiki.encodePageSpecifier( UriBuilder.fromUri(uri), maybeUgly,  pageName );
058    }
059
060
061
062    /** The cache of data from this pollwiki's database.
063      */
064    public WikiCache cache() { return cache; }
065
066
067        private WikiCache cache; // final after init
068
069
070
071    /** The password for the vote-server's wiki accounts, which go by the username Vobot.
072      * These accounts must be created in the pollwiki associated with the vote-server and
073      * in every remote wiki to which users patch differences or otherwise edit texts via
074      * the services of the vote-server.
075      *
076      *     @see ConstructionContext#setPassword(String)
077      */
078    public String password() { return password; }
079
080
081        private final String password;
082
083
084
085    /** The identifier of the revision series, which is zero or greater.  The value is
086      * incremented whenever the vote-server is associated with a new pollwiki, or
087      * whenever the current pollwiki's revision history is altered by reassigning its
088      * revision identifiers.
089      *
090      *     @see ConstructionContext#setRevisionSeries(int)
091      */
092    public int revisionSeries() { return revisionSeries; }
093
094
095        private final int revisionSeries;
096
097
098
099    /** The base location for script execution in the pollwiki, without a trailing slash
100      * (/).  If the pollwiki has a domain name then that name should be used here in its
101      * fully qualified form, even if the wiki is hosted locally along with the
102      * vote-server.  Do not use "localhost" or the short host name alone, otherwise the
103      * vote-server may be unable to login via the MediaWiki API (1.16.1.).  For example
104      * <code>http://reluk.ca/mediawiki</code>.  Script requests may be constructed by
105      * appending the script path and parameters:<pre
106      *
107     *>    scriptURI().toASCIIString() + "/index.php?oldid=1138"</pre>
108      *
109      *     @see ConstructionContext#setScriptLocation(String)
110      *     @see ConstructionContext#setScriptURI(URI)
111      */
112    public URI scriptURI() { return scriptURI; } // cf. votorola.a.web.gwt.App.getScriptLocation()
113
114
115        private final URI scriptURI;
116
117
118
119    /** The base location for requesting pages from the pollwiki.  This is either the
120      * standard access location, such as "{@linkplain #scriptURI() scriptURI}/index.php"
121      * for example, or an alias without a trailing slash (/).  In the latter case
122      * <code>$wgUsePathInfo</code> is assumed to be true.  Page requests formed on this
123      * base should generally not be redirected by the wiki or relays of Crossforum
124      * Theatre state are likely to fail owing to a {@linkplain
125      * votorola.s.gwt.stage.ReferrerRelayer#KEY_HREF URL mismatch}.
126      *
127      *     @see #appendPageSpecifier(String)
128      *     @see #maybeUgly()
129      *     @see ConstructionContext#setLocation(String)
130      *     @see ConstructionContext#setURI(URI)
131      *     @see <a href='http://www.mediawiki.org/wiki/Manual:$wgUsePathInfo'
132      *       target='_top'>$wgUsePathInfo</a>
133      */        // per INLDOC
134    public URI uri() { return uri; } // cf. votorola.a.web.gwt.App.getLocation()
135
136
137        private final URI uri;
138
139
140
141   // - P o l l w i k i ------------------------------------------------------------------
142
143
144    /** @see #uri()
145      * @see #encodePageSpecifier(String)
146      */
147    public StringBuilder appendPageSpecifier( final String encodedPageName )
148    {
149        return MediaWiki.appendPageSpecifier( new StringBuilder(uri.toASCIIString()), maybeUgly,
150          encodedPageName );
151    }
152
153
154
155    /** @see #uri()
156      */
157    public boolean maybeUgly() { return maybeUgly; }
158
159
160        private final boolean maybeUgly;
161
162
163
164    /** {@inheritDoc} This recognizer is based on a snapshot of the pollwiki's interface
165      * messages.  Any change in the relevant messages (unusual) will go undetected by the
166      * recognizer until this PollwikiVS instance is recreated, e.g. by restarting the
167      * vote-server.
168      */
169    public PipeRecognizer pipeRecognizer()
170    {
171        if( pipeRecognizer == null ) // non-atomic test/set may duplicate set, no harm
172        {
173            String pipePrefix = null;
174            String pipeSuffix = null;
175            final URI queryURI;
176            try{ queryURI = new URI( scriptURI() +
177              "/api.php?action=query&meta=allmessages&ammessages=Vo-pipePrefix%7CVo-pipeSuffix&format=xml" ); }
178            catch( URISyntaxException x ) { throw new RuntimeException( x ); }
179
180            logger.fine( "querying pollwiki for pipe-related interface messages: " + queryURI );
181            final Spool spool = new Spool1();
182            read: try
183            {
184                final URLConnection http = queryURI.toURL().openConnection(); // not actually open yet
185                final XMLStreamReader xml = MediaWiki.requestXML( http, spool );
186                while( xml.hasNext() )
187                {
188                    xml.next();
189                    if( !xml.isStartElement() ) continue;
190
191                    if( "allmessages".equals( xml.getLocalName() ))
192                    {
193                        while( xml.hasNext() )
194                        {
195                            xml.next();
196                            if( xml.isEndElement()
197                             && "allmessages".equals( xml.getLocalName() )) break read;
198
199                            if( !xml.isStartElement() ) continue;
200
201                            if( !"message".equals( xml.getLocalName() )) continue;
202
203                            if( xml.getAttributeValue(/*ns*/null,"missing") != null ) continue;
204
205                            final String name = xml.getAttributeValue( /*ns*/null,"name" );
206                            final String value = xml.getElementText(); // moves state to </message>
207                            if( "Vo-pipePrefix".equals( name )) pipePrefix = value;
208                            else if( "Vo-pipeSuffix".equals( name )) pipeSuffix = value;
209                        }
210                    }
211                    MediaWiki.test_error( xml );
212                }
213                throw new MediaWiki.MalformedResponse( "missing 'allmessages' element" );
214            }
215            catch( IOException|XMLStreamException x )
216            {
217                logger.log( LoggerX.WARNING, "unable to obtain pipe configuration", x );
218            }
219            finally{ spool.unwind(); }
220            if( pipePrefix == null || pipeSuffix == null ) pipeRecognizer = new PipeRecognizer0();
221            else pipeRecognizer = new PipeRecognizer1( pipePrefix, pipeSuffix );
222        }
223        return pipeRecognizer;
224    }
225
226
227        private volatile PipeRecognizer pipeRecognizer; /* lazy init to allow recovery in
228          case wiki is initially unreachable */
229
230
231
232    public String positionPageName( final String personName, final String pollName )
233    {
234        return PositionID.pageName( personName, pollName, pipeRecognizer() );
235    }
236
237
238
239   // ====================================================================================
240
241
242    /** A context for configuring the construction of a {@linkplain PollwikiVS PollwikiVS}.
243      */
244    public static @ThreadSafe final class ConstructionContext
245    {
246
247
248        /** Finalizes the configuration.
249          */
250        void postConfigure( final VoteServer.ConstructionContext vsCC ) throws URISyntaxException
251        {
252            if( scriptURI == null )
253            {
254                setScriptLocation( "http://" + vsCC.getServerName() + "/mediawiki" );
255            }
256            if( uri == null ) setLocation( scriptURI.toASCIIString() + "/index.php" );
257        }
258
259
260
261       // --------------------------------------------------------------------------------
262
263
264        /** The base location for requesting wiki pages, or null for the default.
265          *
266          *     @see PollwikiVS#uri()
267          *     @see #setLocation(String)
268          *     @see #setURI(URI)
269          */
270        public URI getURI() { return uri; }
271
272
273            private URI uri = null;
274
275
276            /** Sets the base location for requesting wiki pages.  The default value is
277              * null, which translates at runtime to "http://{@linkplain #getScriptURI()
278              * scriptURI}/index.php".
279              *
280              *     @see PollwikiVS#uri()
281              *     @throws IllegalArgumentException if the URI ends with a slash '/' character.
282              */
283              @ThreadRestricted("constructor")
284            public void setLocation( final String s ) throws URISyntaxException
285            {
286                setURI( new URI( s ));
287            }
288
289
290            /** Sets the base location for requesting wiki pages.  The default value is
291              * null, which translates at runtime to "http://{@linkplain #getScriptURI()
292              * scriptURI}/index.php".
293              *
294              *     @see PollwikiVS#uri()
295              *     @throws IllegalArgumentException if the URI ends with a slash '/' character.
296              */
297            public @ThreadRestricted("constructor") void setURI( final URI u )
298            {
299                if( u.toString().endsWith( "/" ))
300                {
301                    throw new IllegalArgumentException( "URI ends with '/'" );
302                }
303
304                uri = u;
305            }
306
307
308
309        /** The password for this vote-server's wiki account.
310          *
311          *     @see PollwikiVS#password()
312          *     @see #setPassword(String)
313          */
314        public String getPassword() { return password; }
315
316
317            private String password = "insecure password";
318
319
320            /** Sets the password for this vote-server's wiki account, which goes by the
321              * username Vobot.  The default value is "insecure password".
322              *
323              *     @see PollwikiVS#password()
324              */
325              @ThreadRestricted("constructor")
326            public void setPassword( final String p ) { password = p; }
327
328
329
330        /** The identifier of the revision series.
331          *
332          *     @see PollwikiVS#revisionSeries()
333          *     @see #setRevisionSeries(int)
334          */
335        public int getRevisionSeries() { return revisionSeries; }
336
337
338            private int revisionSeries;
339
340
341            /** Sets the identifier of the revision series.  The default value is zero.
342              * If you change it because of a change of pollwiki, then you should also
343              * clear the {@linkplain WikiCache wiki cache}.
344              *
345              *     @see PollwikiVS#revisionSeries()
346              */
347              @ThreadRestricted("constructor")
348            public void setRevisionSeries( final int p ) { revisionSeries = p; }
349
350
351
352        /** The base URI for requesting wiki scripts, or null for the default.
353          *
354          *     @see PollwikiVS#scriptURI()
355          *     @see #setScriptLocation(String)
356          *     @see #setScriptURI(URI)
357          */
358        public URI getScriptURI() { return scriptURI; }
359
360
361            private URI scriptURI = null;
362
363
364            /** Sets the base URI for requesting wiki scripts.  The default value is null,
365              * which translates at runtime to "http://{@linkplain VoteServer#serverName()
366              * serverName}/mediawiki".
367              *
368              *     @see PollwikiVS#scriptURI()
369              *     @throws IllegalArgumentException if the URI ends with a slash '/' character.
370              */
371              @ThreadRestricted("constructor")
372            public void setScriptLocation( final String s ) throws URISyntaxException
373            {
374                setScriptURI( new URI( s ));
375            }
376
377
378            /** Sets the base URI for requesting wiki scripts.  The default value is null,
379              * which translates at runtime to "http://{@linkplain VoteServer#serverName()
380              * serverName}/mediawiki".
381              *
382              *     @see PollwikiVS#scriptURI()
383              *     @throws IllegalArgumentException if the URI ends with a slash '/' character.
384              */
385            public @ThreadRestricted("constructor") void setScriptURI( final URI u )
386            {
387                if( u.toString().endsWith( "/" ))
388                {
389                    throw new IllegalArgumentException( "URI ends with '/'" );
390                }
391
392                scriptURI = u;
393            }
394
395
396    }
397
398
399
400//// P r i v a t e ///////////////////////////////////////////////////////////////////////
401
402
403    private static final Logger logger = LoggerX.i( PollwikiVS.class );
404
405
406}