001package votorola.s.wap; // 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.google.gson.stream.*;
004import java.io.*;
005import java.nio.charset.*;
006import java.util.*;
007import java.util.regex.*;
008import javax.mail.internet.*;
009import javax.servlet.http.*;
010import votorola.a.*;
011import votorola.a.count.*;
012import votorola.a.diff.*;
013import votorola.a.position.*;
014import votorola.a.voter.*;
015import votorola.a.web.wap.*;
016import votorola.g.*;
017import votorola.g.io.*;
018import votorola.g.lang.*;
019import votorola.g.script.*;
020import votorola.g.web.*;
021
022import static votorola.a.voter.IDPair.NOBODY;
023
024
025/** A web API for the {@linkplain DiffCache difference cache}.  Calls are conventionally
026  * prefixed by 'd' (<code>wCall=dDiff</code>).  If you choose another prefix, then adjust
027  * the parameter names below accordingly.  An example request is:
028  *
029  * <blockquote><code><a href='http://reluk.ca:8080/v/wap?wCall=dDiff&amp;dAnchor=Frank-FlippityNet&amp;dA=Georgina-BeenaCom(Mike-ZeleaCom&amp;dB=Test-a-ZeleaCom&amp;wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=dDiff&amp;dAnchor=Frank-FlippityNet&amp;dA=Georgina-BeenaCom(Mike-ZeleaCom&amp;dB=Test-a-ZeleaCom&amp;wPretty</a></code></blockquote>
030  *
031  * <h3 id='diffSpec'>Difference specification parameters</h3>
032  *
033  * <p>Specify which difference (or differences) to return using one or more of the
034  * following parameters.</p>
035  *
036  * <table class='definition' style='margin-left:1em'>
037  *     <tr>
038  *         <th class='key'>Key</th>
039  *         <th>Value</th>
040  *         </tr>
041  *     <tr><td class='key'>dA</td>
042  *
043  *         <td>Specifies the differences between the latest revisions of the anchor
044  *         (draft b) and each of the named authors (draft a).  Authors are named by
045  *         mailish username.  Multiple names are separated by left parentheses '(', as
046  *         for example 'dA=NAME1(NAME2(NAME3'.  Depends on 'dAnchor' and 'dPoll'.</td>
047  *
048  *         </tr>
049  *     <tr><td class='key'>dB</td>
050  *
051  *         <td>Specifies the differences between the latest revisions of the anchor
052  *         (draft a) and each of the named authors (draft b).  Authors are named by
053  *         mailish username.  Multiple names are separated by left parentheses '(', as
054  *         for example 'dB=NAME1(NAME2(NAME3'.  Depends on 'dAnchor' and 'dPoll'.</td>
055  *
056  *         </tr>
057  *     </table>
058  *
059  * <h3>Other query parameters</h3>
060  *
061  * <p>These parameters are specific to the difference cache API.  See also the general
062  * {@linkplain WAP WAP} parameters.</p>
063  *
064  * <table class='definition' style='margin-left:1em'>
065  *     <tr>
066  *         <th class='key'>Key</th>
067  *         <th>Value</th>
068  *         <th>Default</th>
069  *         </tr>
070  *     <tr><td class='key'>dAnchor</td>
071  *
072  *         <td>The anchor specified by mailish username.  The anchor is the refererence
073  *         author for relative difference specifiers such as 'dA' and 'dB'.</td>
074  *
075  *         <td>Null, optional item.</td>
076  *
077  *         </tr>
078  *     <tr><td class='key'>dPoll</td>
079  *
080  *         <td>The poll of the anchor draft specified by {@linkplain Poll#name() poll
081  *         name}.</td>
082  *
083  *         <td>{@value votorola.a.count.Poll#TEST_POLL_NAME}</td>
084  *
085  *         </tr>
086  *     <tr><td class='key'>dPairData</td>
087  *
088  *         <td>Specify 'dPairData' or 'dPairData=y' to include additional data that
089  *         depends on fetching and constructing the {@linkplain DraftPair draft pair} for
090  *         each difference record.  Such fetches are required in any case for 'dA' and
091  *         'dB' differences, so this merely controls whether the data is included in the
092  *         response.</td>
093  *
094  *         <td>'n'</td>
095  *         </tr>
096  *     </table>
097  *
098  * <h3>Response</h3>
099  *
100  * <p>The response includes the following components.  These are shown in JSON format
101  * with explanatory comments:</p><pre
102  *
103 *> {
104  *    "d": { // or other prefix, per {@linkplain WAP wCall} query parameter
105  *
106  *       "error": [ // only if a client-actionable error was detected.
107  *
108  *          "MESSAGE",
109  *          "MESSAGE"
110  *          // and so on
111  *       ],
112  *
113  *       "diff": {
114  *
115  *          // Difference records each indexed by {@linkplain DiffKey difference key}.
116  *
117  *          "KEY": {
118  *
119  *              // Difference record.
120  *
121  *             "aUserMnemonic": "USER MNEMONIC",
122  *               // A short abbreviation of the username of the first draft's author.
123  *               // Depends on query parameter 'dPairData'.
124  *             "aUsername": "USERNAME",
125  *               // The mailish username of the first draft's author.  Depends on query
126  *               // parameter 'dPairData'.
127  *
128  *             "bUserMnemonic": "USER MNEMONIC",
129  *               // A short abbreviation of the username of the second draft's author.
130  *               // Depends on query parameter 'dPairData'.
131  *             "bUsername": "USERNAME",
132  *               // The mailish username of the second draft's author.  Depends on query
133  *               // parameter 'dPairData'.
134  *
135  *             "text": "TEXT"
136  *               // Text of 'diff' output.
137  *          }
138  *          // and so on, for each difference record
139  *       },
140  *
141  *       "diffX": {
142  *
143  *          // Index into difference records.  Only entries specifically requested are
144  *          // included here.
145  *
146  *          "A": [
147  *
148  *             // Results for requested 'dA' differences in the order requested.  Each
149  *             // entry is either a difference key, or null if the difference is unknown.
150  *
151  *             "KEY",
152  *             null,
153  *             "KEY"
154  *             // and so on
155  *          ],
156  *          "B": [
157  *
158  *             // Results for requested 'dB' differences in the order requested.  Each
159  *             // entry is either a difference key, or null if the difference is unknown.
160  *
161  *             "KEY",
162  *             null,
163  *             "KEY"
164  *             // and so on
165  *          ]
166  *       },
167  *
168  *       "anchorMnemonic": "USER MNEMONIC",
169  *         // A short abbreviation of the anchor's username.  Depends on query
170  *         // parameters 'dAnchor' and 'dPairData'.
171  *    }
172  * }</pre>
173  *
174  * <p>For a baseline reference, here is a minimal pretty request together with the
175  * corresponding response:
176  * <code><a href='http://reluk.ca:8080/v/wap?wCall=dDiff&amp;wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=dDiff&amp;wPretty</a></code></p><pre
177 *> {
178  *    "d": {
179  *       "diff": {},
180  *       "diffX": {}
181  *    }
182  * }</pre>
183  *
184  * <p>Responses are intended to be {@linkplain Uncached uncached} when requesting
185  * relative differences such as <a href='#diffSpec'>dA or dB</a>.</p>
186  */
187public final @ThreadRestricted("constructor") class DiffWAP extends Call
188{
189
190
191    /** Constructs a DiffWAP.
192      *
193      *     @see #prefix()
194      *     @see #req()
195      */
196    public DiffWAP( final String prefix, final Requesting req,
197      final ResponseConfiguration resConfig ) throws HTTPRequestException
198    {
199        super( prefix, req, resConfig );
200        final HttpServletRequest reqHS = req.request();
201        boolean isCacheable = true; // till proven otherwise
202        toAddPairData = HTTPServletRequestX.getBooleanParameter( "dPairData", reqHS );
203        final HashMap<String,DraftPair> pairMap = new HashMap<>();
204          // map keyed by local a-page name, all pairs have anchor as b-draft
205        final String pollName;
206        {
207            final String dPoll = HTTPServletRequestX.getParameterNonEmpty( "dPoll", reqHS );
208            pollName = dPoll == null? Poll.TEST_POLL_NAME: dPoll;
209        }
210
211      // Parse difference specifications.
212      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
213        final VoteServer.Run vsRun = req.wap().vsRun();
214        final PollwikiVS wiki = vsRun.voteServer().pollwiki();
215        String specName;
216        String specShortName;
217        String spec;
218
219      // dA
220      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
221        specShortName = "A";
222        specName = prefix() + specShortName;
223        spec = HTTPServletRequestX.getParameterNonEmpty( specName, reqHS );
224        final List<String> pageNameListA;
225        final List<DraftPair> pairXListA;
226        if( spec == null )
227        {
228            pageNameListA = Collections.emptyList();
229            pairXListA = Collections.emptyList();
230        }
231        else
232        {
233            pageNameListA = new ArrayList<>();
234            pairXListA = new ArrayList<>();
235            isCacheable = false;
236            initSpecAB( specName, spec, pollName, pageNameListA, pairMap, wiki );
237            pairXListMapPut( specShortName, pairXListA );
238        }
239
240      // dB
241      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
242        specShortName = "B";
243        specName = prefix() + specShortName;
244        spec = HTTPServletRequestX.getParameterNonEmpty( specName, reqHS );
245        final List<String> pageNameListB;
246        final List<DraftPair> pairXListB;
247        if( spec == null )
248        {
249            pageNameListB = Collections.emptyList();
250            pairXListB = Collections.emptyList();
251        }
252        else
253        {
254            pageNameListB = new ArrayList<>();
255            pairXListB = new ArrayList<>();
256            isCacheable = false;
257            initSpecAB( specName, spec, pollName, pageNameListB, pairMap, wiki );
258            pairXListMapPut( specShortName, pairXListB );
259        }
260
261      // Construct draft pairs and index them.
262      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
263        try
264        {
265            final int pageCount = pairMap.size();
266            if( pageCount > 0 )
267            {
268                final ArrayList<String> pageNameList = new ArrayList<String>( pageCount
269                  + 1 // what DraftPair.newDraftPairs will add
270                  + 1 ); // spare room
271                pageNameList.addAll( pairMap.keySet() );
272                final String bPageName = wiki.positionPageName( anchorID(reqHS).username(),
273                  pollName ); // not actually b-draft in all cases, pairs later reversed as necessary
274                final LinkedList<DraftPair> pairList = new LinkedList<DraftPair>();
275                errorList = DraftPair.newDraftPairs( pageNameList, bPageName, vsRun,
276                  /*countSource*/null, pairList, errorList );
277                for( final DraftPair pair: pairList )
278                {
279                    pairMap.put( /*key*/pair.aCore().pageName(), /*value*/pair );
280                }
281                List<DraftPair> pairXList;
282
283              // dA
284              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
285                pairXList = pairXListA;
286                for( final String pageName: pageNameListA )
287                {
288                    final DraftPair pair = pairMap.get( pageName );
289                    if( pair == null ) pairXList.add( null );
290                    else
291                    {
292                        pairXList.add( pair );
293                        pairSetAdd( pair );
294                    }
295                }
296
297              // dB
298              // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
299                pairXList = pairXListB;
300                for( final String pageName: pageNameListB )
301                {
302                    DraftPair pair = pairMap.get( pageName );
303                    if( pair == null ) pairXList.add( null );
304                    else
305                    {
306                        pair = pair.newReversePair();
307                        pairXList.add( pair );
308                        pairSetAdd( pair );
309                    }
310                }
311            }
312        }
313        catch( final IOException x ) { throw new RuntimeException( x ); }
314
315        if( !isCacheable )
316        {
317            resConfig.headNoCache();
318            resConfig.headMustRevalidate(); // don't use stale values
319        }
320    }
321
322
323
324    private void initSpecAB( final String specName, final String spec, final String pollName,
325      final List<String> pageNameList, final Map<String,DraftPair> pairMap, final PollwikiVS wiki )
326    {
327        final Matcher m = USERNAME_QUERY_ITEM_PATTERN.matcher( spec );
328        while( m.find() )
329        {
330            final String username = m.group( 1 );
331            final IDPair user;
332            try{ user = IDPair.fromUsername( username ); }
333            catch( final AddressException x )
334            {
335                errorEnlist( new VotorolaException( specName + "=" + username
336                  + ": not a mailish username: " + x ));
337                pageNameList.add( null ); // placeholder for sake of diffX
338                continue;
339            }
340
341            final String pageName = wiki.positionPageName( user.username(), pollName );
342            pageNameList.add( pageName );
343            pairMap.put( pageName, null ); // pair to be constructed later
344        }
345    }
346
347
348
349   // ------------------------------------------------------------------------------------
350
351
352    /** The name to use in the {@link WAP wCall} query parameter, which is {@value}.  For
353      * example: <code>wCall=dDiff</code>.
354      */
355    public static final String CALL_TYPE = "Diff";
356
357
358
359    /** The pattern of a single mailish username in a list-form query value, such as
360      * "Jack-ThisOrg(Jill-ThatNet(Up HillCom".  Use matcher.{@linkplain Matcher#find()
361      * find}() to scan the list; it will set group 1 to each username in turn.
362      */
363    static final Pattern USERNAME_QUERY_ITEM_PATTERN = Pattern.compile(
364      "([^ ()][^()]*[^ ()])(?:[ ()]*[()][ ()]*|$)" );
365    //  USERNAME              SEPARATOR
366    //
367    // '(' is the item separator.  It was chosen because it is illegal (or at least not
368    // recommended) in email addresses and requires no encoding in URLs.  It is a weird
369    // separator so the matching is somewhat lenient here (though clients should not count
370    // on this).  Each separator may actually consist of any combination of one or more
371    // left parentheses '(' and/or right parentheses ')' mixed with any number of space
372    // characters.
373    //
374    // http://tools.ietf.org/html/rfc822
375    // http://tools.ietf.org/html/rfc1035#section-2.3.1
376    // http://tools.ietf.org/html/rfc2396#section-2.3
377
378
379
380   // - C a l l --------------------------------------------------------------------------
381
382
383    public void respond( final Responding res ) throws IOException
384    {
385        final JsonWriter out = res.outJSON();
386        out.name( prefix() ).beginObject();
387
388      // error
389      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
390        if( errorList != null )
391        {
392            out.name( "error" ).beginArray();
393            for( final Throwable t: errorList ) out.value( t.toString() );
394            out.endArray();
395        }
396
397      // diff
398      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
399        out.name( "diff" ).beginObject();
400        final DiffCache diffCache = res.wap().vsRun().voteServer().diffCache();
401        final Writer outBuf = res.outBuf();
402        final JSONStringWriter outString = new JSONStringWriter( outBuf );
403          // "need not be closed"
404        final boolean wPretty = res.wPretty();
405        final String indent = res.outJSONIndent();
406        for( final DraftPair pair: pairSet )
407        {
408            final String key = pair.diffKeyParse().key();
409            final File diffFile = diffCache.diffFile( pair );
410            out.name( key ).beginObject();
411
412          // aUserMnemonic, aUsername
413          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
414            if( toAddPairData )
415            {
416                out.name( "aUserMnemonic" ).value( pair.aUserMnemonic() );
417                out.name( "aUsername" ).value( pair.aCore().person().username() );
418            }
419
420          // bUserMnemonic, bUsername
421          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
422            if( toAddPairData )
423            {
424                out.name( "bUserMnemonic" ).value( pair.bUserMnemonic() );
425                out.name( "bUsername" ).value( pair.bCore().person().username() );
426            }
427
428          // text
429          // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
430            out.flush();
431            outBuf.append( "," );
432            if( wPretty )
433            {
434                outBuf.append( '\n' );
435                int i = 0;
436                do { outBuf.append( indent ); ++i; } while( i < 4 );
437            }
438            outBuf.append( "\"text\":" ); if( wPretty ) outBuf.append( ' ' );
439            outBuf.append( '"' );
440            {
441                FileX.appendTo( outString, diffFile, Charset.defaultCharset() );
442             // outString.flush(); // redundant as it "need not be flushed"
443            }
444            outBuf.append( '"' );
445
446            out.endObject();
447        }
448        out.endObject();
449
450      // diffX
451      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
452        out.name( "diffX" ).beginObject();
453        for( final Map.Entry<String,List<DraftPair>> entry: pairXListEntrySet() )
454        {
455            out.name( entry.getKey() ).beginArray();
456            for( final DraftPair pair: entry.getValue() )
457            {
458                if( pair == null ) out.nullValue();
459                else out.value( pair.diffKeyParse().key() );
460            }
461            out.endArray();
462        }
463        out.endObject();
464
465      // AnchorMnemonic
466      // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
467        if( toAddPairData )
468        {
469            try
470            {
471                out.name( "anchorMnemonic" ).value( IDPair.buildUserMnemonic(
472                  anchorID(res.request()).username(), new StringBuilder() ).toString() );
473            }
474            catch( HTTPRequestException x ) {} // no problem, skip it
475        }
476
477      // ` ` `
478        out.endObject();
479    }
480
481
482
483//// P r i v a t e ///////////////////////////////////////////////////////////////////////
484
485
486    /** @return the identifier of the anchor, or IDPair.NOBODY if the value of the
487      *   'dAnchor' request parameter is present but invalid.  Any validation failure is
488      *   reported via errorEnlist().
489      *
490      * @throws HTTPRequestException if the 'dAnchor' query parameter is entirely missing.
491      */
492    private IDPair anchorID( final HttpServletRequest reqHS ) throws HTTPRequestException
493    {
494        if( anchorIDX != null ) throw anchorIDX;
495
496        if( anchorID == null )
497        {
498            try
499            {
500                final String username = HTTPServletRequestX.getParameterRequired( "dAnchor", reqHS );
501                try{ anchorID = IDPair.fromUsername( username ); }
502                catch( final AddressException x )
503                {
504                    anchorID = IDPair.NOBODY;
505                    errorEnlist( new VotorolaException( "'dAnchor=" + username
506                      + "' is not a mailish username: " + x ));
507                }
508            }
509            catch( final HTTPRequestException x )
510            {
511                anchorIDX = x;
512                throw x;
513            }
514        }
515
516        return anchorID;
517    }
518
519
520        private IDPair anchorID;
521
522
523        private HTTPRequestException anchorIDX; // cached
524
525
526
527    private List<Throwable> errorList; // lazily constructed
528
529
530        private void errorEnlist( final Throwable x )
531        {
532            errorList = ThrowableX.listedThrowable( x, errorList );
533        }
534
535
536
537    private Set<DraftPair> pairSet = Collections.emptySet(); // till needed
538
539
540        private boolean pairSetEmpty = true;
541
542
543        private void pairSetAdd( final DraftPair pair )
544        {
545            if( pairSetEmpty ) // lazily construct it:
546            {
547                final int initCapacity = 40; // guess
548                pairSet = new HashSet<DraftPair>( (int)((initCapacity + 1) / 0.75f) + 1 );
549                pairSetEmpty = false;
550            }
551            pairSet.add( pair );
552        }
553
554
555
556    private Iterable<Map.Entry<String,List<DraftPair>>> pairXListEntrySet()
557    {
558        return pairXListMap.entrySet();
559    }
560
561
562        private Map<String,List<DraftPair>> pairXListMap = Collections.emptyMap(); // till needed
563
564
565        private boolean pairXListMapEmpty = true;
566
567
568        /** @param pairXList a list of pairs for the diffX index.
569          */
570        private void pairXListMapPut( final String specShortName, final List<DraftPair> pairXList )
571        {
572            if( pairXListMapEmpty ) // lazily construct it:
573            {
574                final int initCapacity = /*dA*/1 + /*dB*/1 + /*buffer*/5;
575                pairXListMap = new HashMap<>( (int)((initCapacity + 1) / 0.75f) + 1 );
576                pairXListMapEmpty = false;
577            }
578            pairXListMap.put( specShortName, pairXList );
579        }
580
581
582
583    private final boolean toAddPairData;
584
585
586}