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}