package votorola.s.wap.store; // Copyright 2012, 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.sql.*; import java.util.*; import java.util.regex.*; import javax.servlet.*; import javax.servlet.http.*; import votorola.a.web.wap.*; import votorola.g.lang.*; import votorola.g.web.*; /** A web API for the ephemeral storage of unimportant data. Exclusive stores are * assigned to clients based on HTTP signature, but exclusivity is not guaranteed. Two * clients may happen to have the same signature and thus be assigned the same store. * Nor is the duration of storage guaranteed. This facility is intended for short-lived * data of low importance and especially for sharing data across domains, as for instance * between a referrer and its target document. It therefore supplements cookies and Web * Storage which cannot be shared across domains. An example request is: * *
http://reluk.ca:8080/v/wap?wCall=sStore&sPut=mykey'myvalue&wPretty
*
* These parameters are specific to the store API. See also the general {@linkplain
* WAP WAP} parameters. Calls are conventionally prefixed by 's'
* (wCall=sStore
). If you choose a different prefix, then adjust the
* parameter names below accordingly.
Key | *Value | *Action | *
---|---|---|
sGet | * *A storage key, optionally followed by a guard comprising an apostrophe (') * and a value as in "sGet=address'23+Main+Street". The key may not include an * apostrophe, but the guard value may. The guard value may also be empty. | * *If no guard is specified, then it returns the currently stored value, * which may be null. If a guard is specified, then the action depends on * whether the guard value matches the stored value. If it matches, then the * stored value is returned as usual and is also deleted from the store; * otherwise null is returned and the stored value is not deleted. This is * currently the only method of deleting individual values. | * *
sPut | * *A single key/value pair separated by an apostrophe ('), as in * "sPut=address'23+Main+Street". If multiple apostrophes are present, then the * first is taken as the separator. The key may not include an apostrophe, but * the value may. The value may also be empty. | * *Stores the value under the key and returns the same value. | * *
Any of the above parameters may be specified multiple times to multiple effect. A * request specifying "sGet=name&sGet=address", for example, will yield the values of * both "name" and "address".
* *The response includes the following components. These are shown in JSON format * with explanatory comments:
{ * "s": { // or other prefix, per {@linkplain WAP wCall} query parameter * "value": { // keyed values: * "KEY": "VALUE", // the typical value is a string * "KEY": "", // the string may be empty * "KEY": null // it may also be null, meaning nothing at all is stored * // and so on * } * } * }* *
The response headers are set to {@linkplain ResponseConfiguration#headNoCache() * forbid client caching} and the {@linkplain ResponseConfiguration#headMustRevalidate() * use of stale responses}. However these headers are not necessarily obeyed by all * clients. Consider therefore adding a nonce * to all requests as a fallback.
*/ public @ThreadRestricted("constructor") final class StoreWAP extends Call { public static @ThreadSafe void init( final WAP wap ) throws ServletException { try { table = new StoreTable( wap ); } catch( SQLException x ) { throw new ServletException( x ); } } /** Constructs a StoreWAP. * * @see #prefix() * @see #req() */ public StoreWAP( final String prefix, final Requesting req, final ResponseConfiguration resConfig ) throws HTTPRequestException { super( prefix, req, resConfig ); final HttpServletRequest reqHS = req.request(); final String client = clientSignature( reqHS ); try { String reqName; // Put. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - reqName = prefix() + "Put"; final String[] putArray = reqHS.getParameterValues( reqName ); if( putArray != null ) for( String put: putArray ) { final Matcher m = PUT_PATTERN.matcher( put ); if( !m.matches() ) { throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST, "malformed query parameter: " + reqName + "=" + put ); } final String key = m.group( 1 ); if( key.length() > LIMIT_KEY ) { throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST, "(" + reqName + ") key too long: " + key ); } final String value = m.group( 2 ); if( value.length() > LIMIT_VALUE ) { throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST, "(" + reqName + ") value too long: " + value ); } table.put( client, key, value ); valueMap.put( key, value ); } // Get. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - reqName = prefix() + "Get"; final String[] getArray = reqHS.getParameterValues( reqName ); if( getArray != null ) for( String get: getArray ) { final Matcher m = GET_PATTERN.matcher( get ); if( !m.matches() ) { throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST, "malformed query parameter: " + reqName + "=" + get ); } final String key = m.group( 1 ); String value = m.group( 2 ); if( value == null ) value = table.get( client, key ); // ordinary get else if( !table.delete( client, key, value )) value = null; // guarded get valueMap.put( key, value ); } } catch( SQLException x ) { throw new RuntimeException( x ); } resConfig.headNoCache(); resConfig.headMustRevalidate(); // don't use stale values } // ------------------------------------------------------------------------------------ /** The name to use in the {@link WAP wCall} query parameter, which is {@value}. For * example:wCall=sStore
.
*/
public static final String CALL_TYPE = "Store";
/** Returns the signature of the requesting client.
*/
public static String clientSignature( final HttpServletRequest reqHS )
{
final StringBuilder b = new StringBuilder();
b.append( HTTPServletRequestX.getForwardedRemoteAddr( reqHS ));
b.append( ' ' );
b.append( reqHS.getHeader( "User-Agent" ));
b.append( ' ' );
b.append( reqHS.getHeader( "Accept-Language" ));
if( b.length() > LIMIT_CLIENT ) b.setLength( LIMIT_CLIENT );
return b.toString();
}
// - C a l l --------------------------------------------------------------------------
public void respond( final Responding res ) throws java.io.IOException
{
final JsonWriter out = res.outJSON();
out.name( prefix() ).beginObject();
out.name( "value" ).beginObject();
for( final Map.Entry