package 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. import com.google.gson.stream.*; import java.io.*; import java.nio.charset.*; import java.util.*; import java.util.regex.*; import javax.mail.internet.*; import javax.servlet.http.*; import votorola.a.*; import votorola.a.count.*; import votorola.a.diff.*; import votorola.a.position.*; import votorola.a.voter.*; import votorola.a.web.wap.*; import votorola.g.*; import votorola.g.io.*; import votorola.g.lang.*; import votorola.g.script.*; import votorola.g.web.*; import static votorola.a.voter.IDPair.NOBODY; /** A web API for the {@linkplain DiffCache difference cache}. Calls are conventionally * prefixed by 'd' (wCall=dDiff). If you choose another prefix, then adjust * the parameter names below accordingly. An example request is: * *
http://reluk.ca:8080/v/wap?wCall=dDiff&dAnchor=Frank-FlippityNet&dA=Georgina-BeenaCom(Mike-ZeleaCom&dB=Test-a-ZeleaCom&wPretty
* *

Difference specification parameters

* *

Specify which difference (or differences) to return using one or more of the * following parameters.

* * * * * * * * * * * * * * * * *
KeyValue
dASpecifies the differences between the latest revisions of the anchor * (draft b) and each of the named authors (draft a). Authors are named by * mailish username. Multiple names are separated by left parentheses '(', as * for example 'dA=NAME1(NAME2(NAME3'. Depends on 'dAnchor' and 'dPoll'.
dBSpecifies the differences between the latest revisions of the anchor * (draft a) and each of the named authors (draft b). Authors are named by * mailish username. Multiple names are separated by left parentheses '(', as * for example 'dB=NAME1(NAME2(NAME3'. Depends on 'dAnchor' and 'dPoll'.
* *

Other query parameters

* *

These parameters are specific to the difference cache API. See also the general * {@linkplain WAP WAP} parameters.

* * * * * * * * * * * * * * * * * * * * * * * * * * * *
KeyValueDefault
dAnchorThe anchor specified by mailish username. The anchor is the refererence * author for relative difference specifiers such as 'dA' and 'dB'.Null, optional item.
dPollThe poll of the anchor draft specified by {@linkplain Poll#name() poll * name}.{@value votorola.a.count.Poll#TEST_POLL_NAME}
dPairDataSpecify 'dPairData' or 'dPairData=y' to include additional data that * depends on fetching and constructing the {@linkplain DraftPair draft pair} for * each difference record. Such fetches are required in any case for 'dA' and * 'dB' differences, so this merely controls whether the data is included in the * response.'n'
* *

Response

* *

The response includes the following components. These are shown in JSON format * with explanatory comments:

 {
  *    "d": { // or other prefix, per {@linkplain WAP wCall} query parameter
  *
  *       "error": [ // only if a client-actionable error was detected.
  *
  *          "MESSAGE",
  *          "MESSAGE"
  *          // and so on
  *       ],
  *
  *       "diff": {
  *
  *          // Difference records each indexed by {@linkplain DiffKey difference key}.
  *
  *          "KEY": {
  *
  *              // Difference record.
  *
  *             "aUserMnemonic": "USER MNEMONIC",
  *               // A short abbreviation of the username of the first draft's author.
  *               // Depends on query parameter 'dPairData'.
  *             "aUsername": "USERNAME",
  *               // The mailish username of the first draft's author.  Depends on query
  *               // parameter 'dPairData'.
  *
  *             "bUserMnemonic": "USER MNEMONIC",
  *               // A short abbreviation of the username of the second draft's author.
  *               // Depends on query parameter 'dPairData'.
  *             "bUsername": "USERNAME",
  *               // The mailish username of the second draft's author.  Depends on query
  *               // parameter 'dPairData'.
  *
  *             "text": "TEXT"
  *               // Text of 'diff' output.
  *          }
  *          // and so on, for each difference record
  *       },
  *
  *       "diffX": {
  *
  *          // Index into difference records.  Only entries specifically requested are
  *          // included here.
  *
  *          "A": [
  *
  *             // Results for requested 'dA' differences in the order requested.  Each
  *             // entry is either a difference key, or null if the difference is unknown.
  *
  *             "KEY",
  *             null,
  *             "KEY"
  *             // and so on
  *          ],
  *          "B": [
  *
  *             // Results for requested 'dB' differences in the order requested.  Each
  *             // entry is either a difference key, or null if the difference is unknown.
  *
  *             "KEY",
  *             null,
  *             "KEY"
  *             // and so on
  *          ]
  *       },
  *
  *       "anchorMnemonic": "USER MNEMONIC",
  *         // A short abbreviation of the anchor's username.  Depends on query
  *         // parameters 'dAnchor' and 'dPairData'.
  *    }
  * }
* *

For a baseline reference, here is a minimal pretty request together with the * corresponding response: * http://reluk.ca:8080/v/wap?wCall=dDiff&wPretty

 {
  *    "d": {
  *       "diff": {},
  *       "diffX": {}
  *    }
  * }
* *

Responses are intended to be {@linkplain Uncached uncached} when requesting * relative differences such as dA or dB.

*/ public final @ThreadRestricted("constructor") class DiffWAP extends Call { /** Constructs a DiffWAP. * * @see #prefix() * @see #req() */ public DiffWAP( final String prefix, final Requesting req, final ResponseConfiguration resConfig ) throws HTTPRequestException { super( prefix, req, resConfig ); final HttpServletRequest reqHS = req.request(); boolean isCacheable = true; // till proven otherwise toAddPairData = HTTPServletRequestX.getBooleanParameter( "dPairData", reqHS ); final HashMap pairMap = new HashMap<>(); // map keyed by local a-page name, all pairs have anchor as b-draft final String pollName; { final String dPoll = HTTPServletRequestX.getParameterNonEmpty( "dPoll", reqHS ); pollName = dPoll == null? Poll.TEST_POLL_NAME: dPoll; } // Parse difference specifications. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final VoteServer.Run vsRun = req.wap().vsRun(); final PollwikiVS wiki = vsRun.voteServer().pollwiki(); String specName; String specShortName; String spec; // dA // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` specShortName = "A"; specName = prefix() + specShortName; spec = HTTPServletRequestX.getParameterNonEmpty( specName, reqHS ); final List pageNameListA; final List pairXListA; if( spec == null ) { pageNameListA = Collections.emptyList(); pairXListA = Collections.emptyList(); } else { pageNameListA = new ArrayList<>(); pairXListA = new ArrayList<>(); isCacheable = false; initSpecAB( specName, spec, pollName, pageNameListA, pairMap, wiki ); pairXListMapPut( specShortName, pairXListA ); } // dB // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` specShortName = "B"; specName = prefix() + specShortName; spec = HTTPServletRequestX.getParameterNonEmpty( specName, reqHS ); final List pageNameListB; final List pairXListB; if( spec == null ) { pageNameListB = Collections.emptyList(); pairXListB = Collections.emptyList(); } else { pageNameListB = new ArrayList<>(); pairXListB = new ArrayList<>(); isCacheable = false; initSpecAB( specName, spec, pollName, pageNameListB, pairMap, wiki ); pairXListMapPut( specShortName, pairXListB ); } // Construct draft pairs and index them. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - try { final int pageCount = pairMap.size(); if( pageCount > 0 ) { final ArrayList pageNameList = new ArrayList( pageCount + 1 // what DraftPair.newDraftPairs will add + 1 ); // spare room pageNameList.addAll( pairMap.keySet() ); final String bPageName = wiki.positionPageName( anchorID(reqHS).username(), pollName ); // not actually b-draft in all cases, pairs later reversed as necessary final LinkedList pairList = new LinkedList(); errorList = DraftPair.newDraftPairs( pageNameList, bPageName, vsRun, /*countSource*/null, pairList, errorList ); for( final DraftPair pair: pairList ) { pairMap.put( /*key*/pair.aCore().pageName(), /*value*/pair ); } List pairXList; // dA // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` pairXList = pairXListA; for( final String pageName: pageNameListA ) { final DraftPair pair = pairMap.get( pageName ); if( pair == null ) pairXList.add( null ); else { pairXList.add( pair ); pairSetAdd( pair ); } } // dB // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` pairXList = pairXListB; for( final String pageName: pageNameListB ) { DraftPair pair = pairMap.get( pageName ); if( pair == null ) pairXList.add( null ); else { pair = pair.newReversePair(); pairXList.add( pair ); pairSetAdd( pair ); } } } } catch( final IOException x ) { throw new RuntimeException( x ); } if( !isCacheable ) { resConfig.headNoCache(); resConfig.headMustRevalidate(); // don't use stale values } } private void initSpecAB( final String specName, final String spec, final String pollName, final List pageNameList, final Map pairMap, final PollwikiVS wiki ) { final Matcher m = USERNAME_QUERY_ITEM_PATTERN.matcher( spec ); while( m.find() ) { final String username = m.group( 1 ); final IDPair user; try{ user = IDPair.fromUsername( username ); } catch( final AddressException x ) { errorEnlist( new VotorolaException( specName + "=" + username + ": not a mailish username: " + x )); pageNameList.add( null ); // placeholder for sake of diffX continue; } final String pageName = wiki.positionPageName( user.username(), pollName ); pageNameList.add( pageName ); pairMap.put( pageName, null ); // pair to be constructed later } } // ------------------------------------------------------------------------------------ /** The name to use in the {@link WAP wCall} query parameter, which is {@value}. For * example: wCall=dDiff. */ public static final String CALL_TYPE = "Diff"; /** The pattern of a single mailish username in a list-form query value, such as * "Jack-ThisOrg(Jill-ThatNet(Up HillCom". Use matcher.{@linkplain Matcher#find() * find}() to scan the list; it will set group 1 to each username in turn. */ static final Pattern USERNAME_QUERY_ITEM_PATTERN = Pattern.compile( "([^ ()][^()]*[^ ()])(?:[ ()]*[()][ ()]*|$)" ); // USERNAME SEPARATOR // // '(' is the item separator. It was chosen because it is illegal (or at least not // recommended) in email addresses and requires no encoding in URLs. It is a weird // separator so the matching is somewhat lenient here (though clients should not count // on this). Each separator may actually consist of any combination of one or more // left parentheses '(' and/or right parentheses ')' mixed with any number of space // characters. // // http://tools.ietf.org/html/rfc822 // http://tools.ietf.org/html/rfc1035#section-2.3.1 // http://tools.ietf.org/html/rfc2396#section-2.3 // - C a l l -------------------------------------------------------------------------- public void respond( final Responding res ) throws IOException { final JsonWriter out = res.outJSON(); out.name( prefix() ).beginObject(); // error // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( errorList != null ) { out.name( "error" ).beginArray(); for( final Throwable t: errorList ) out.value( t.toString() ); out.endArray(); } // diff // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` out.name( "diff" ).beginObject(); final DiffCache diffCache = res.wap().vsRun().voteServer().diffCache(); final Writer outBuf = res.outBuf(); final JSONStringWriter outString = new JSONStringWriter( outBuf ); // "need not be closed" final boolean wPretty = res.wPretty(); final String indent = res.outJSONIndent(); for( final DraftPair pair: pairSet ) { final String key = pair.diffKeyParse().key(); final File diffFile = diffCache.diffFile( pair ); out.name( key ).beginObject(); // aUserMnemonic, aUsername // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( toAddPairData ) { out.name( "aUserMnemonic" ).value( pair.aUserMnemonic() ); out.name( "aUsername" ).value( pair.aCore().person().username() ); } // bUserMnemonic, bUsername // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( toAddPairData ) { out.name( "bUserMnemonic" ).value( pair.bUserMnemonic() ); out.name( "bUsername" ).value( pair.bCore().person().username() ); } // text // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` out.flush(); outBuf.append( "," ); if( wPretty ) { outBuf.append( '\n' ); int i = 0; do { outBuf.append( indent ); ++i; } while( i < 4 ); } outBuf.append( "\"text\":" ); if( wPretty ) outBuf.append( ' ' ); outBuf.append( '"' ); { FileX.appendTo( outString, diffFile, Charset.defaultCharset() ); // outString.flush(); // redundant as it "need not be flushed" } outBuf.append( '"' ); out.endObject(); } out.endObject(); // diffX // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` out.name( "diffX" ).beginObject(); for( final Map.Entry> entry: pairXListEntrySet() ) { out.name( entry.getKey() ).beginArray(); for( final DraftPair pair: entry.getValue() ) { if( pair == null ) out.nullValue(); else out.value( pair.diffKeyParse().key() ); } out.endArray(); } out.endObject(); // AnchorMnemonic // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( toAddPairData ) { try { out.name( "anchorMnemonic" ).value( IDPair.buildUserMnemonic( anchorID(res.request()).username(), new StringBuilder() ).toString() ); } catch( HTTPRequestException x ) {} // no problem, skip it } // ` ` ` out.endObject(); } //// P r i v a t e /////////////////////////////////////////////////////////////////////// /** @return the identifier of the anchor, or IDPair.NOBODY if the value of the * 'dAnchor' request parameter is present but invalid. Any validation failure is * reported via errorEnlist(). * * @throws HTTPRequestException if the 'dAnchor' query parameter is entirely missing. */ private IDPair anchorID( final HttpServletRequest reqHS ) throws HTTPRequestException { if( anchorIDX != null ) throw anchorIDX; if( anchorID == null ) { try { final String username = HTTPServletRequestX.getParameterRequired( "dAnchor", reqHS ); try{ anchorID = IDPair.fromUsername( username ); } catch( final AddressException x ) { anchorID = IDPair.NOBODY; errorEnlist( new VotorolaException( "'dAnchor=" + username + "' is not a mailish username: " + x )); } } catch( final HTTPRequestException x ) { anchorIDX = x; throw x; } } return anchorID; } private IDPair anchorID; private HTTPRequestException anchorIDX; // cached private List errorList; // lazily constructed private void errorEnlist( final Throwable x ) { errorList = ThrowableX.listedThrowable( x, errorList ); } private Set pairSet = Collections.emptySet(); // till needed private boolean pairSetEmpty = true; private void pairSetAdd( final DraftPair pair ) { if( pairSetEmpty ) // lazily construct it: { final int initCapacity = 40; // guess pairSet = new HashSet( (int)((initCapacity + 1) / 0.75f) + 1 ); pairSetEmpty = false; } pairSet.add( pair ); } private Iterable>> pairXListEntrySet() { return pairXListMap.entrySet(); } private Map> pairXListMap = Collections.emptyMap(); // till needed private boolean pairXListMapEmpty = true; /** @param pairXList a list of pairs for the diffX index. */ private void pairXListMapPut( final String specShortName, final List pairXList ) { if( pairXListMapEmpty ) // lazily construct it: { final int initCapacity = /*dA*/1 + /*dB*/1 + /*buffer*/5; pairXListMap = new HashMap<>( (int)((initCapacity + 1) / 0.75f) + 1 ); pairXListMapEmpty = false; } pairXListMap.put( specShortName, pairXList ); } private final boolean toAddPairData; }