001    /*
002    Copyright (c) 1996-2011, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.webSvr.threeD;
031    
032    import java.io.IOException;
033    import java.util.Arrays;
034    import java.util.Collections;
035    import java.util.List;
036    
037    import javax.servlet.ServletConfig;
038    import javax.servlet.ServletContext;
039    import javax.servlet.ServletException;
040    import javax.servlet.ServletOutputStream;
041    import javax.servlet.http.HttpServlet;
042    import javax.servlet.http.HttpServletRequest;
043    import javax.servlet.http.HttpServletResponse;
044    
045    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
046    import org.hd.d.pg2k.svrCore.CoreConsts;
047    import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
048    import org.hd.d.pg2k.svrCore.Name;
049    import org.hd.d.pg2k.svrCore.Tuple;
050    import org.hd.d.pg2k.svrCore.MIME.ExhibitMIME;
051    import org.hd.d.pg2k.svrCore.stats.StatsLogger;
052    import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
053    import org.hd.d.pg2k.webSvr.util.WebUtils;
054    
055    
056    /**3D Walkthrough data servlet.
057     * Supports 3D walkthrough app by servicing its simple, small, incremental
058     * data requests.
059     * <p>
060     * All requests are GET requests.
061     * <p>
062     * Also contains static client-side send/receive code
063     * to keep everything in one place.
064     */
065    public final class DataServlet extends HttpServlet
066        {
067        /**Unhook from servlet logger. */
068        @Override
069        public void destroy()
070            {
071            logger.setContext(null); // Stop using servlet logger.
072            super.destroy();
073            }
074    
075        /**Hook into servlet logger. */
076        @Override
077        public void init(final ServletConfig servletConfig)
078            throws ServletException
079            {
080            logger.setContext(servletConfig.getServletContext()); // Use servlet.log().
081            super.init(servletConfig);
082            }
083    
084        /**Our logger which falls back to System.out if servlet log not available; never null. */
085        private final WebUtils.ServletLoggerWithFallback logger = new WebUtils.ServletLoggerWithFallback();
086    
087        /**The stats set to which we log general tunnel servlet stats.
088         * The unique codes are the constants DSVNAME_XXX.
089         */
090        private final StatsLogger.StatsConfig statsIDDSV =
091            new StatsLogger.StatsConfig("3DWTSERVLET",
092                                        logger, // Use servlet log if poss.
093                                        false, // Only dump summaries...
094                                        12 * 3600, // About every 12 hours.
095                                        true); // Adaptive.
096    
097        /**General stats event name: inbound HTTP request. */
098        public static final String DSVNAME_HTTPREQUEST = "HTTP-request";
099    
100        /**General stats event name: failures handling input HTTP requests. */
101        public static final String DSVNAME_HTTPFAIL = "HTTP-failure";
102    
103        /**General stats event name: inbound HTTP request rejected by request op-code. */
104        public static final String DSVNAME_HTTPREJOP = "HTTP-reqRejectedByOp";
105    
106    
107    
108    
109    
110        /**Get singleton (per-servlet-context) data pipeline/cache instance.
111         * The config param must not be null, but for some operations
112         * (such as calling destroy()) request can be null.
113         * <p>
114         * This does not cache its return value.
115         */
116        private static DataSourceBean getDataSourceBean(
117                final ServletContext ctxt,
118                final HttpServletRequest request)
119            {
120            // Fetches/creates the data source...
121            final DataSourceBean dataSource =
122                DataSourceBean.getApplicationInstance(ctxt);
123    
124            // Ensure that the essential details are set up.
125            dataSource.setServletContext(ctxt);
126            if(request != null)
127                { dataSource.setContextPath(request.getContextPath()); }
128    
129            return(dataSource);
130            }
131    
132    
133        /**Respond to a GET request from 3DWT clients.
134         *
135         * @exception IOException if an input/output error occurs
136         */
137        @Override
138        public void doGet(final HttpServletRequest request,
139                          final HttpServletResponse response)
140            throws IOException
141            {
142            // Don't allow this response to be cached.
143            // HTTP/1.0 headers.
144            response.setHeader("Expires", "0");
145            response.setHeader("Pragma", "no-cache");
146            // HTTP/1.1 headers.
147            response.setHeader("Cache-Control", "no-cache");
148    
149    //if(IsDebug.isDebug) { System.out.println("[Received 3DWT request...]"); }
150    //if(IsDebug.isDebug) { logger.log("[Received 3DWT request...]"); }
151    
152            // Note HTTP the request...
153            StatsLogger.captureDataPoint(statsIDDSV, DSVNAME_HTTPREQUEST);
154    
155            final DataSourceBean ds = getDataSourceBean(getServletContext(), request);
156    
157            // Get the current set of 3DWT exhibit data...
158            final Tuple.Pair<LightweightMetaDataFetchInterface.GalleryBasicMetaData, List<Name.ExhibitFull>> data = getData(ds);
159    
160            // The (printable 7-bit ASCII) result string...
161            final String result;
162    
163            // Get operation type.
164            final String opType = request.getParameter(Utils.HP_OPCODE);
165            if(Utils.OP_LUS.equals(opType)) // Most common request, so do first.
166                {
167                // Request for name lookup.
168                try
169                    {
170                    final List<Name.ExhibitFull> names = data.second;
171                    final String ordinals = request.getParameter(Utils.HP_ORD);
172                    final String[] nums = ordinals.split(",");
173                    // Ignore requested items beyond the maximum allowed per call.
174                    final int nResults = Math.min(nums.length, LightweightMetaDataFetchInterface.MAX_NAMES_REQUEST);
175                    final Name.ExhibitFull results[] = new Name.ExhibitFull[nResults];
176                    int len = nResults-1; // For the separators...
177                    for(int i = nResults; --i >= 0; )
178                        {
179                        final Name.ExhibitFull n = names.get(Integer.parseInt(nums[i], 10));
180                        if(n == null) { throw new IOException("bad index"); }
181                        results[i] = n;
182                        len += n.length();
183                        }
184                    // Separate the results with commas, like the inputs.
185                    // Null items are not allowed.
186                    final StringBuilder sb = new StringBuilder(len);
187                    for(int i = 0; i < nResults; ++i)
188                        {
189                        if(i != 0) { sb.append(','); }
190                        sb.append(results[i]);
191                        }
192                    result = sb.toString();
193                    }
194                catch(final Exception e)
195                    {
196                    // Any parameter problem generates a tidy error...
197                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
198                    return;
199                    }
200                }
201    //        else if(Utils.OP_LU.equals(opType)) // Next most common request...
202    //            {
203    //            // Request for name lookup.
204    //            try
205    //                {
206    //                final List<String> names = data.second;
207    //                result = names.get(Integer.parseInt(request.getParameter(Utils.HP_ORD), 10));
208    //                }
209    //            catch(final Exception e)
210    //                {
211    //                // Any parameter problem generates a tidy error...
212    //                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
213    //                return;
214    //                }
215    //            }
216            else if(Utils.OP_MD.equals(opType))
217                {
218                // Request for metadata.
219                result = data.first.toWireString();
220                }
221            else
222                {
223                // Missing/wrong op-type generates an error.
224                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
225                return;
226                }
227    
228            // Write (printable 7-bit ASCII) output in one operation.
229            final int length = result.length();
230            response.setContentLength(length);
231            final byte buf[] = new byte[length];
232            for(int i = length; --i >= 0; ) { buf[i] = (byte) result.charAt(i); }
233            final ServletOutputStream os = response.getOutputStream();
234            os.write(buf);
235            os.flush();
236    
237    //if(IsDebug.isDebug) { System.out.println("[Replied to 3DWT request.]"); }
238    //if(IsDebug.isDebug) { logger.log("[Replied to 3DWT request.]"); }
239            }
240    
241    
242        /**Get the filtered data associated with the current DataSourceBean; never null.
243         * A new value is created on first use and when the AEP changes.
244         * <p>
245         * This call should always be very fast
246         * except when doing the initial filtering/construction of the exhibit list
247         * when there would be a danger of races and redundant resource usage,
248         * so we serialise calls to eliminate this possibility.
249         */
250        private static synchronized Tuple.Pair<LightweightMetaDataFetchInterface.GalleryBasicMetaData, List<Name.ExhibitFull>> getData(final DataSourceBean dsb)
251            throws IOException
252            {
253            assert(dsb != null);
254    
255            Tuple.Pair<LightweightMetaDataFetchInterface.GalleryBasicMetaData, List<Name.ExhibitFull>> result;
256            while((result = (Tuple.Pair<LightweightMetaDataFetchInterface.GalleryBasicMetaData, List<Name.ExhibitFull>>) dsb.getAEPLinkedValue(WTKey)) == null)
257                {
258                final AllExhibitProperties aep = dsb.getAllExhibitProperties(-1);
259                final Name.ExhibitFull[] rawNames = aep.select(new AllExhibitProperties.AEPFilter()
260                    {
261                    /**Only accept results that the 3DWT app can handle.
262                     * @return  true if the exhibit is suitable
263                     */
264                    public final boolean accept(final AllExhibitProperties aep,
265                                                final Name.ExhibitFull exhibitName)
266                        {
267                        final ExhibitMIME.ExhibitTypeParameters exhibitType = ExhibitMIME.getInputFileType(exhibitName);
268                        if(!Utils.supportedTypeFor3DWT(exhibitType)) { return(false); }
269                        return(true);
270                        }
271                    }, null, 0);
272                Arrays.sort(rawNames, ExhibitAttrUtils.getAttrWords().SMART_ORDER);
273                // Create read-only smart-sorted exhibit name List...
274                final List<Name.ExhibitFull> exhibits = Collections.unmodifiableList(Arrays.asList(rawNames));
275                dsb.putIfAbsentAEPLinkedValue(WTKey,
276                    new Tuple.Pair<LightweightMetaDataFetchInterface.GalleryBasicMetaData, List<Name.ExhibitFull>>(
277                        new LightweightMetaDataFetchInterface.GalleryBasicMetaData(aep.aeid.timestamp ^ exhibits.size(), exhibits.size(), CoreConsts.DEFAULT_TEMPORAL_SLACKNESS_S * 500),
278                        exhibits));
279                }
280    
281            return(result);
282            }
283    
284        /**Key for 3DWT-linked data in DataSourceBean. */
285        private static final DataSourceBean.AEPLinkedKey WTKey = new DataSourceBean.AEPLinkedKey("3DWT");
286    
287        /**Unique Serialisation class ID generated by http://random&#46;hd&#46;org/. */
288        private static final long serialVersionUID = -1276945667652232880L;
289        }