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&sPut=mykey'myvalue&wPretty" target='_top'>http://reluk.ca:8080/v/wap?wCall=sStore&sPut=mykey'myvalue&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}