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&dAnchor=Frank-FlippityNet&dA=Georgina-BeenaCom(Mike-ZeleaCom&dB=Test-a-ZeleaCom&wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=dDiff&dAnchor=Frank-FlippityNet&dA=Georgina-BeenaCom(Mike-ZeleaCom&dB=Test-a-ZeleaCom&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&wPretty' target='_top'>http://reluk.ca:8080/v/wap?wCall=dDiff&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}