001package 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.
002
003import com.google.gson.stream.*;
004import java.sql.*;
005import java.util.*;
006import java.util.regex.*;
007import javax.servlet.*;
008import javax.servlet.http.*;
009import votorola.a.web.wap.*;
010import votorola.g.lang.*;
011import votorola.g.web.*;
012
013
014/** A web API for the ephemeral storage of unimportant data.  Exclusive stores are
015  * assigned to clients based on HTTP signature, but exclusivity is not guaranteed.  Two
016  * clients may happen to have the same signature and thus be assigned the same store.
017  * Nor is the duration of storage guaranteed.  This facility is intended for short-lived
018  * data of low importance and especially for sharing data across domains, as for instance
019  * between a referrer and its target document.  It therefore supplements cookies and Web
020  * Storage which cannot be shared across domains.  An example request is:
021  *
022  * <blockquote><code><a href="http://reluk.ca:8080/v/wap?wCall=sStore&amp;sPut=mykey'myvalue&amp;wPretty" target='_top'>http://reluk.ca:8080/v/wap?wCall=sStore&amp;sPut=mykey'myvalue&amp;wPretty</a></code></blockquote>
023  *
024  * <h3>Query parameters</h3>
025  *
026  * <p>These parameters are specific to the store API.  See also the general {@linkplain
027  * WAP WAP} parameters.  Calls are conventionally prefixed by 's'
028  * (<code>wCall=sStore</code>).  If you choose a different prefix, then adjust the
029  * parameter names below accordingly.</p>
030  *
031  * <table class='definition' style='margin-left:1em'>
032  *     <tr>
033  *         <th class='key'>Key</th>
034  *         <th>Value</th>
035  *         <th>Action</th>
036  *         </tr>
037  *     <tr><td class='key'>sGet</td>
038  *
039  *         <td>A storage key, optionally followed by a guard comprising an apostrophe (')
040  *         and a value as in "sGet=address'23+Main+Street".  The key may not include an
041  *         apostrophe, but the guard value may.  The guard value may also be empty.</td>
042  *
043  *         <td>If no guard is specified, then it returns the currently stored value,
044  *         which may be null.  If a guard is specified, then the action depends on
045  *         whether the guard value matches the stored value.  If it matches, then the
046  *         stored value is returned as usual and is also deleted from the store;
047  *         otherwise null is returned and the stored value is not deleted.  This is
048  *         currently the only method of deleting individual values.</td>
049  *
050  *         </tr>
051  *     <tr><td class='key'>sPut</td>
052  *
053  *         <td>A single key/value pair separated by an apostrophe ('), as in
054  *         "sPut=address'23+Main+Street".  If multiple apostrophes are present, then the
055  *         first is taken as the separator.  The key may not include an apostrophe, but
056  *         the value may.  The value may also be empty.</td>
057  *
058  *         <td>Stores the value under the key and returns the same value.</td>
059  *
060  *         </tr>
061  *     </table>
062  *
063  *  <p>Any of the above parameters may be specified multiple times to multiple effect.  A
064  *  request specifying "sGet=name&sGet=address", for example, will yield the values of
065  *  both "name" and "address".</p>
066  *
067  * <h3>Response</h3>
068  *
069  * <p>The response includes the following components.  These are shown in JSON format
070  * with explanatory comments:</p><pre
071  *
072 *> {
073  *    "s": { // or other prefix, per {@linkplain WAP wCall} query parameter
074  *       "value": { // keyed values:
075  *          "KEY": "VALUE", // the typical value is a string
076  *          "KEY": "",      // the string may be empty
077  *          "KEY": null     // it may also be null, meaning nothing at all is stored
078  *          // and so on
079  *       }
080  *    }
081  * }</pre>
082  *
083  * <p>The response headers are set to {@linkplain ResponseConfiguration#headNoCache()
084  * forbid client caching} and the {@linkplain ResponseConfiguration#headMustRevalidate()
085  * use of stale responses}.  However these headers are not necessarily obeyed by all
086  * clients.  Consider therefore <a href='../../../a/web/wap/WAP.html#wNonce' target='_top'>adding a nonce</a>
087  * to all requests as a fallback.</p>
088  */
089public @ThreadRestricted("constructor") final class StoreWAP extends Call
090{
091
092
093    public static @ThreadSafe void init( final WAP wap ) throws ServletException
094    {
095        try { table = new StoreTable( wap ); }
096        catch( SQLException x ) { throw new ServletException( x ); }
097    }
098
099
100
101    /** Constructs a StoreWAP.
102      *
103      *     @see #prefix()
104      *     @see #req()
105      */
106    public StoreWAP( final String prefix, final Requesting req,
107      final ResponseConfiguration resConfig ) throws HTTPRequestException
108    {
109        super( prefix, req, resConfig );
110        final HttpServletRequest reqHS = req.request();
111        final String client = clientSignature( reqHS );
112        try
113        {
114            String reqName;
115
116          // Put.
117          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
118            reqName = prefix() + "Put";
119            final String[] putArray = reqHS.getParameterValues( reqName );
120            if( putArray != null ) for( String put: putArray )
121            {
122                final Matcher m = PUT_PATTERN.matcher( put );
123                if( !m.matches() )
124                {
125                    throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST,
126                      "malformed query parameter: " + reqName + "=" + put );
127                }
128
129                final String key = m.group( 1 );
130                if( key.length() > LIMIT_KEY )
131                {
132                    throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST,
133                      "(" + reqName + ") key too long: " + key );
134                }
135
136                final String value = m.group( 2 );
137                if( value.length() > LIMIT_VALUE )
138                {
139                    throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST,
140                      "(" + reqName + ") value too long: " + value );
141                }
142
143                table.put( client, key, value );
144                valueMap.put( key, value );
145            }
146
147          // Get.
148          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
149            reqName = prefix() + "Get";
150            final String[] getArray = reqHS.getParameterValues( reqName );
151            if( getArray != null ) for( String get: getArray )
152            {
153                final Matcher m = GET_PATTERN.matcher( get );
154                if( !m.matches() )
155                {
156                    throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST,
157                      "malformed query parameter: " + reqName + "=" + get );
158                }
159
160                final String key = m.group( 1 );
161                String value = m.group( 2 );
162                if( value == null ) value = table.get( client, key ); // ordinary get
163                else if( !table.delete( client, key, value )) value = null; // guarded get
164                valueMap.put( key, value );
165            }
166        }
167        catch( SQLException x ) { throw new RuntimeException( x ); }
168        resConfig.headNoCache();
169        resConfig.headMustRevalidate(); // don't use stale values
170    }
171
172
173
174   // ------------------------------------------------------------------------------------
175
176
177    /** The name to use in the {@link WAP wCall} query parameter, which is {@value}.  For
178      * example: <code>wCall=sStore</code>.
179      */
180    public static final String CALL_TYPE = "Store";
181
182
183
184    /** Returns the signature of the requesting client.
185      */
186    public static String clientSignature( final HttpServletRequest reqHS )
187    {
188        final StringBuilder b = new StringBuilder();
189        b.append( HTTPServletRequestX.getForwardedRemoteAddr( reqHS ));
190        b.append( ' ' );
191        b.append( reqHS.getHeader( "User-Agent" ));
192        b.append( ' ' );
193        b.append( reqHS.getHeader( "Accept-Language" ));
194        if( b.length() > LIMIT_CLIENT ) b.setLength( LIMIT_CLIENT );
195        return b.toString();
196    }
197
198
199
200   // - C a l l --------------------------------------------------------------------------
201
202
203    public void respond( final Responding res ) throws java.io.IOException
204    {
205        final JsonWriter out = res.outJSON();
206        out.name( prefix() ).beginObject();
207        out.name( "value" ).beginObject();
208        for( final Map.Entry<String,String> entry: valueMap.entrySet() )
209        {
210            out.name( entry.getKey() ).value( entry.getValue() );
211        }
212        out.endObject(); // value
213        out.endObject(); // prefix
214    }
215
216
217
218//// P r i v a t e ///////////////////////////////////////////////////////////////////////
219
220
221    /** The pattern of a get request.  Sets groups (1) key and (2) clearance value (which
222      * may be null).
223      */
224    private static final Pattern GET_PATTERN = Pattern.compile( "(.+?)(?:'(.*))?" );
225                                                              //  KEY      CLEARANCE VALUE
226
227
228    private static final int LIMIT_CLIENT = 1_000;
229
230    private static final int LIMIT_KEY = 200;
231
232    private static final int LIMIT_VALUE = 4_000;
233
234
235
236    /** The pattern of a put request.  Sets groups (1) key and (2) value.
237      */
238    private static final Pattern PUT_PATTERN = Pattern.compile( "(.+?)'(.*)" );
239                                                              //  KEY   VALUE
240
241
242    private static volatile StoreTable table; // final after init
243
244
245
246    private HashMap<String,String> valueMap = new HashMap<>(
247      /*initial capacity*/(int)((4 + 1) / 0.75f) + 1 );
248
249
250}