001package votorola.a.web.wap; // Copyright 2011-2013, 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.logging.*;
008import java.util.regex.*;
009import javax.servlet.*;
010import javax.servlet.http.*;
011import votorola.a.*;
012import votorola.a.web.wic.*;
013import votorola.g.lang.*;
014import votorola.g.logging.*;
015import votorola.g.web.*;
016import votorola.s.wap.*;
017import votorola.s.wap.store.*;
018
019
020/** A web API for Votorola's servlet container (Tomcat).  It provides an HTTP interface
021  * for remote application programmers.  A single instance of WAP is constructed by the
022  * container as defined in the <code><a
023  * href='../../../../../../../a/web/context/WEB-INF/web.xml'>web.xml</a></code>
024  * configuration file.  The service path is <code>/wap</code>.  An example request is:
025  *
026  * <blockquote><pre><a href='http://reluk.ca:8080/v/wap?wCall=cCount&amp;cPoll=G%2Fp%2Fsandbox&amp;wPretty'
027  *                          >http://reluk.ca:8080/v/wap?wCall=cCount&amp;cPoll=G%2Fp%2Fsandbox&amp;wPretty</a></pre></blockquote>
028  *
029  * <h3>Query parameters</h3>
030  *
031  * <table class='definition' style='margin-left:1em'>
032  *     <tr>
033  *         <th class='key'>Key</th>
034  *         <th>Value</th>
035  *         <th>Default</th>
036  *         </tr>
037  *     <tr><td class='key'>wCall</td>
038  *
039  *         <td>The list of {@linkplain Call typed calls} for this request, separated by
040  *         apostrophes (').  For example, "wCall=hHarvest'psPollspace" specifies call
041  *         types Harvest and Pollspace under prefixes 'h' and 'ps' respectively.
042  *         Prefixes provide namespacing of query parameters and results that are
043  *         particular to each call type.  They are restricted to lower case ASCII
044  *         characters, excepting the single letter prefix 'w' which is reserved.</td>
045  *
046  *         <td>Null, making no calls.</td>
047  *
048  *         </tr>
049  *     <tr><td class='key'>wCallback</td>
050  *
051  *         <td>The name of the callback function for a JSONP response, or leave it
052  *         unspecified for a plain JSON response.  This is a qualifier for wForm whenever
053  *         that parameter is set to 'JSON'.</td>
054  *
055  *         <td>Null, specifying a plain JSON response.</td>
056  *
057  *         </tr>
058  *     <tr><td class='key'>wForm</td>
059  *
060  *         <td>The form of the response.  Currently only 'JSON' is supported, which gives
061  *         either JSON or JSONP depending on the value of wCallback.</td>
062  *
063  *         <td>'JSON'</td>
064  *
065  *         </tr>
066  *     <tr id='wNonce'><td class='key'>wNonce</td>
067  *
068  *         <td>An arbitrary value intended to give the request a unique URL.  Use it to
069  *         defeat caching in clients that ignore the {@linkplain
070  *         ResponseConfiguration#headNoCache() cache control headers}.</td>
071  *
072  *         <td>Null, optional item.</td>
073  *
074  *         </tr>
075  *     <tr><td class='key'>wPretty</td>
076  *
077  *         <td>Specify 'wPretty' or 'wPretty=y' to make the response more human readable.
078  *         This adds whitespace and forces a content type of 'text/plain'.</td>
079  *
080  *         <td>'n'</td>
081  *         </tr>
082  *     </table>
083  *
084  * <h3>Response</h3>
085  *
086  * <p>The response has the following general form, shown here with JSON formatting and
087  * explanatory comments.  For further details, see the API documentation of the individual
088  * {@linkplain Call calls}.</p><pre
089  *
090 *> {
091  *    "CALL-PREFIX-1": {
092  *       // response to first call
093  *     },
094  *    "CALL-PREFIX-2": {
095  *       // response to second call
096  *    }
097  *    // and so on, for each call
098  *}</pre>
099  *
100  *     @see <a href='http://reluk.ca/w/Stuff:Pollwiki' target='_top'>Pollwiki web APIs</a>
101  */
102public final @ThreadSafe class WAP extends HttpServlet
103{
104
105
106    public @Override void init() throws ServletException
107    {
108        try
109        {
110            vsRun = new VoteServer( VOWicket.contextPathToVoteServerName( // OPT for space: if whole vote-server really needed, servlets ought to share it
111              getServletContext().getContextPath() )).new Run( /*isSingleThreaded*/false );
112            vsRun.init_done(); // nothing to do here anymore
113
114            synchronized( WAP.this )
115            {
116                callConstructors.put( CountWAP.CALL_TYPE, CountWAP.class );
117                callConstructors.put( DiffWAP.CALL_TYPE, DiffWAP.class );
118                callConstructors.put( StoreWAP.CALL_TYPE, StoreWAP.class );
119                callConstructors.put( HarvestWAP.CALL_TYPE, HarvestWAP.class );
120                callConstructors.put( KickWAP.CALL_TYPE, KickWAP.class );
121                callConstructors.put( PollspaceWAP.CALL_TYPE, PollspaceWAP.class );
122            }
123        }
124        catch( final IOException | ReflectiveOperationException | javax.script.ScriptException
125          | java.sql.SQLException | java.net.URISyntaxException x )
126        {
127            throw new ServletException( x );
128        }
129    }
130
131
132
133   // ------------------------------------------------------------------------------------
134
135
136    /** Map of constructors for all known call types, keyed by type name.
137      */
138      @Warning("thread restricted object, holds WAP.this")
139    Map<String,Constructor<? extends Call>> callConstructors() { return callConstructors; }
140      // OPT: this could be made thread-safe by touch-sync and guaranteeing immutability
141
142
143        private final ConstructorMap callConstructors = new ConstructorMap();
144
145
146
147    /** The run of the vote-server against which requests are made.
148      */
149    public VoteServer.Run vsRun() { return vsRun; }
150
151
152        private volatile VoteServer.Run vsRun; // final after init
153
154
155
156    /** The pattern of a call entry in query parameter 'wCall'.  Sets groups (1) call
157      * prefix and (2) call type.
158      */
159    static final Pattern W_CALL_PATTERN = Pattern.compile(
160      "([a-vxyz]|[a-z]{2,})([A-Z][a-zA-Z_0-9]*)(?:'|$)" );
161    //  PREFIX              CALL-TYPE             SEPAR
162    //
163    // The apostrophe separator (') was chosen because it is unreserved in a URI:
164    // http://tools.ietf.org/html/rfc2396#section-2.3
165
166
167
168   // - H t t p - S e r v l e t ----------------------------------------------------------
169
170
171    protected @Override void doGet( final HttpServletRequest req, final HttpServletResponse res )
172      throws IOException
173    {
174        try
175        {
176            final Level level = Level.FINE;
177            if( logger.isLoggable(level) )
178            {
179                logger.log( level, HTTPServletRequestX.buildRequestSummary(req).toString() );
180            }
181            new Responding( WAP.this, req, res ).respond();
182        }
183        catch( final Exception x )
184        {
185            if( !res.isCommitted() )
186            {
187                if( x instanceof HTTPRequestException )
188                {
189                    res.sendError( ((HTTPRequestException)x).code(), x.toString() );
190                    return; // client's error, no need to log it here
191                }
192
193                res.sendError( /*500*/HttpServletResponse.SC_INTERNAL_SERVER_ERROR, x.toString() );
194            }
195            logger.log( LoggerX.WARNING, "Error in service of request " + req.getQueryString(), x );
196        }
197    }
198
199
200
201//// P r i v a t e ///////////////////////////////////////////////////////////////////////
202
203
204    private static final Logger logger = LoggerX.i( WAP.class );
205
206
207
208   // ====================================================================================
209
210
211    private final class ConstructorMap extends HashMap<String,Constructor<? extends Call>>
212    {
213
214        private void put( final String callType, final Class<? extends Call> cl )
215          throws ReflectiveOperationException
216        {
217            final Object oldValue = super.put( callType, cl.getConstructor(
218              String.class, Requesting.class, ResponseConfiguration.class ));
219            if( oldValue != null ) throw new IllegalStateException(); // duplicate call type
220
221            cl.getMethod("init",WAP.class).invoke( /*static, no object*/null, WAP.this );
222        }
223
224
225       // - M a p ------------------------------------------------------------------------
226
227
228        public @Override Constructor<? extends Call> put( final String callType,
229          final Constructor<? extends Call> call )
230        {
231            throw new UnsupportedOperationException();
232        }
233
234    }
235
236
237}