001package votorola.a.web.wap; // Copyright 2011-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.io.*; 005import java.lang.reflect.*; 006import java.util.*; 007import java.util.regex.*; 008import javax.servlet.http.*; 009import votorola.g.*; 010import votorola.g.lang.*; 011import votorola.g.web.*; 012 013 014/** A context for responding to one or more calls. 015 */ 016public @ThreadRestricted("Call constructor") final class Responding 017 implements Requesting, ResponseConfiguration 018{ 019 020 021 /** Constructs a Responding. 022 * 023 * @see #wap() 024 * @see #request() 025 * @see #response() 026 */ 027 Responding( WAP _wap, HttpServletRequest _request, HttpServletResponse _response ) 028 throws HTTPRequestException 029 { 030 request = _request; 031 response = _response; 032 wap = _wap; 033 034 wCallback = HTTPServletRequestX.getParameterNonEmpty( "wCallback", request ); 035 wPretty = HTTPServletRequestX.getBooleanParameter( "wPretty", request ); 036 final String form = HTTPServletRequestX.getParameterNonEmpty( "wForm", request ); 037 if( form != null && !"JSON".equals( form )) 038 { 039 throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST, 040 "unrecognized wForm value: " + form ); 041 } 042 043 final String wCall = HTTPServletRequestX.getParameterNonEmpty( "wCall", request ); 044 if( wCall == null ) calls = Collections.emptyList(); 045 else 046 { 047 calls = new ArrayList<Call>( /*initial capacity*/wCall.length() / 4 + 1 ); 048 final Matcher m = WAP.W_CALL_PATTERN.matcher( wCall ); 049 synchronized( wap ) // per WAP.callConstructors 050 { 051 for( int n = 0; m.find(); ++n ) // each entry in wCall 052 { 053 final String callType = m.group( 2 ); 054 final Constructor<? extends Call> callConstructor = 055 wap.callConstructors().get( callType ); 056 if( callConstructor == null ) 057 { 058 throw new HTTPRequestException( /*400*/HttpServletResponse.SC_BAD_REQUEST, 059 "wCall to unknown call type: " + callType ); 060 } 061 062 final String prefix = m.group( 1 ); 063 try 064 { 065 calls.add( callConstructor.newInstance( prefix, /*req*/Responding.this, 066 /*resConfig*/Responding.this )); 067 } 068 catch( final InvocationTargetException xIT ) 069 { 070 Throwable x = xIT.getCause(); 071 if( x instanceof HTTPRequestException ) throw (HTTPRequestException)x; 072 073 if( x == null ) x = xIT; // probably never occurs 074 else // nor probably do these: 075 { 076 if( x instanceof Error ) throw (Error)x; 077 078 if( !( x instanceof Exception )) throw new IllegalStateException( x ); 079 } 080 081 throw VotorolaRuntimeException.castOrWrapped( (Exception)x ); 082 } 083 catch( Exception x ) { throw new RuntimeException( x ); } // others not much expected 084 } 085 } 086 } 087 } 088 089 090 091 // -------------------------------------------------------------------------------- 092 093 094 /** The writer for the response. The bulk of the response is likely to be written 095 * indirectly through the tributary buffer {@linkplain #outJSON() outJSON}. In any 096 * case, be sure to flush outJSON before writing directly to outBuf. 097 */ 098 public @Warning("flush outJSON") Writer outBuf() { return outBuf; } 099 100 101 private Writer outBuf; // final after init in respond() 102 103 104 105 /** A JSON writer based on {@linkplain #outBuf() outBuf}. Be sure to bracket the 106 * response of each particular call in its assigned prefix, as shown here:<pre> 107 * 108 * res.outJSON().name( {@linkplain Call#prefix() prefix}() ).beginObject(); 109 * // output your response here 110 * res.outJSON().endObject();</pre> 111 */ 112 public JsonWriter outJSON() { return outJSON; } 113 114 115 private JsonWriter outJSON; // final after init in respond() 116 117 118 119 /** The indentation unit for the JSON writer (e.g. 3 or 4 spaces), or an empty string 120 * "" if no indentation is required. 121 */ 122 public String outJSONIndent() { return outJSONIndent; } 123 124 125 private String outJSONIndent; // final after init in respond() 126 127 128 129 /** Responds to all calls. 130 */ 131 void respond() throws IOException 132 { 133 final String characterEncoding = "UTF-8"; 134 response.setCharacterEncoding( characterEncoding ); 135 try 136 ( 137 final Writer outBuf = new BufferedWriter( new OutputStreamWriter( 138 response.getOutputStream(), characterEncoding )); 139 // note that response.isCommitted() when outBuf flushes 140 final JsonWriter outJSON = new JsonWriter( outBuf ); // grep GSONCLOSE for reason 141 ){ 142 Responding.this.outBuf = outBuf; 143 Responding.this.outJSON = outJSON; 144 if( wCallback != null ) 145 { 146 outBuf.append( wCallback ); 147 outBuf.append( '(' ); 148 } 149 final String contentType; 150 if( wPretty ) 151 { 152 contentType = "text/plain"; 153 outJSONIndent = PRETTY_INDENT; 154 outJSON.setIndent( outJSONIndent ); // otherwise it would be packed 155 } 156 else 157 { 158 contentType = wCallback == null? "application/json": "application/javascript"; 159 outJSONIndent = ""; 160 } 161 response.setContentType( contentType ); 162 outJSON.beginObject(); 163 for( final Call call: calls ) call.respond( Responding.this ); 164 outJSON.endObject(); 165 if( wCallback != null ) 166 { 167 outJSON.flush(); 168 outBuf.append( ");" ); 169 } 170 } 171 } 172 173 174 175 /** The HTTP response to the client. 176 */ 177 public HttpServletResponse response() { return response; } 178 179 180 private final HttpServletResponse response; 181 182 183 184 // - R e q u e s t i n g -------------------------------------------------------------- 185 186 187 public HttpServletRequest request() { return request; } 188 189 190 private final HttpServletRequest request; 191 192 193 194 public WAP wap() { return wap; } 195 196 197 private final WAP wap; 198 199 200 201 public boolean wPretty() { return wPretty; } 202 203 204 private final boolean wPretty; 205 206 207 208 // - R e s p o n s e - C o n f i g u r a t i o n -------------------------------------- 209 210 211 public void headMustRevalidate() 212 { 213 assert !response.isCommitted(); 214 if( headMustRevalidate_called ) return; 215 216 headMustRevalidate_called = true; 217 response.addHeader( "Cache-Control", "must-revalidate" ); 218 } 219 220 221 private boolean headMustRevalidate_called; 222 223 224 225 public void headNoCache() 226 { 227 assert !response.isCommitted(); 228 if( headNoCache_called ) return; 229 230 // cf. org.apache.wicket.request.http.WebResponse.disableCaching 231 // http://stackoverflow.com/questions/3976624/java-servlet-how-to-disable-caching-of-page 232 headNoCache_called = true; 233 response.addHeader( "Cache-Control", "no-cache" ); 234 response.addHeader( "Cache-Control", "no-store" ); 235 // response.addHeader( "Cache-Control", "private" ); // Chrome TEST per ResponseConfiguration 236 response.addHeader( "Pragma", "no-cache" ); 237 // response.setDateHeader( "Expires", 0L ); // Chrome TEST per ResponseConfiguration 238 } 239 240 241 private boolean headNoCache_called; 242 243 244 245//// P r i v a t e /////////////////////////////////////////////////////////////////////// 246 247 248 private final List<Call> calls; 249 250 251 252 /** The indentation string for all pretty responses. 253 */ 254 private static final String PRETTY_INDENT = " "; 255 256 257 258 private final String wCallback; 259 260 261 262}