001    package org.hd.d.pg2k.webSvr.catalogue;
002    
003    import java.io.IOException;
004    import java.util.List;
005    import java.util.Map;
006    
007    import javax.servlet.ServletContext;
008    import javax.servlet.ServletException;
009    import javax.servlet.http.HttpServlet;
010    import javax.servlet.http.HttpServletRequest;
011    import javax.servlet.http.HttpServletResponse;
012    
013    import org.hd.d.pg2k.svrCore.ExhibitName;
014    import org.hd.d.pg2k.svrCore.LocaleBeanBase;
015    import org.hd.d.pg2k.svrCore.Name;
016    import org.hd.d.pg2k.svrCore.TextUtils;
017    import org.hd.d.pg2k.webSvr.exhibit.Expr;
018    import org.hd.d.pg2k.webSvr.exhibit.TreeFilterBean;
019    import org.hd.d.pg2k.webSvr.util.Breadcrumbs;
020    import org.hd.d.pg2k.webSvr.util.LocaleBean;
021    import org.hd.d.pg2k.webSvr.util.WebConsts;
022    
023    /**
024     * Created by IntelliJ IDEA.
025     * User: Damon Hart-Davis
026     * Date: 04-Jul-2003
027     * Time: 20:23:55
028     * To change this template use Options | File Templates.
029     */
030    
031    /**Servlet to do filtering and selection of one tree of exhibits for display.
032     * Sub-classes must implement getTitleCommonCatalogueName() and getFilter().
033     */
034    public abstract class TreeFilterServletBase extends HttpServlet
035        {
036        /**Unique serial ID. */
037        private static final long serialVersionUID = 2376699413596151320L;
038        /**Bean that holds cache of selected data in tree-form for us.
039         * Only created at init().
040         */
041        private final TreeFilterBean tfb = new TreeFilterBean();
042    
043        /**Handle a GET request.
044         * Ultimately we will internally redirect to the tree-display JSP, unless:
045         * <ul>
046         * <li>There is an error, eg the request URI is malformed or impossible.
047         * <li>We canonicalise the request with a redirection.
048         * </ul>
049         */
050        @Override
051        protected void doGet(final HttpServletRequest request,
052                             final HttpServletResponse response)
053            throws ServletException, IOException
054            {
055            // Get the base URI to pass to the display JSP/servlet.
056            final String baseURI = request.getServletPath();
057    
058            // Extract tail of request containing embedded word sequence prefix,
059            // ensuring that it is always non-null and
060            // ends with some file component (to make parsing easier)...
061            String tailURI = request.getPathInfo();
062            if(tailURI == null) { tailURI = "/"; }
063    
064            // Convert this into a word-sequence prefix.
065            Name wsPrefix = Name.EMPTY;
066            try { wsPrefix = TreeFilterBean.convertTrailingURIToWordSequence(tailURI); }
067            catch(final IllegalArgumentException e) // User entered malformed input.
068                {
069                // Mark malformed input as "not found".
070                //response.sendError(response.SC_NOT_FOUND);
071                request.setAttribute("javax.servlet.error.request_uri", tailURI);
072                request.getRequestDispatcher(WebConsts.NOT_FOUND_PAGE).forward(request, response);
073                return;
074                }
075    
076    //System.err.println("baseURI: " + baseURI);
077    //System.err.println("tailURI: " + tailURI);
078    //System.err.println("wsPrefix: " + wsPrefix);
079    
080            // Look up given prefix; if null it is an invalid URL.
081            // Try for the root of this index instead.
082            final ServletContext context = getServletContext();
083            List<CharSequence> nodeContent = tfb.selectPrefixesAndShortNames(context, wsPrefix, null);
084            boolean[] uninterestingNodes;
085            Map<CharSequence,Integer> exhibitCounts;
086            if(nodeContent == null)
087                {
088                // Try using the top node of the tree instead.
089                // If this is still null, the display page will understand that it has no entries.
090                // Note that the exhibit names are in short form to sort properly with the prefixes.
091                nodeContent = tfb.selectPrefixesAndShortNames(context, "", null);
092                uninterestingNodes = tfb.getUninterestingNodes(context, "", null, true);
093                exhibitCounts = tfb.getExhibitCount(context, "", null);
094                // Reset the prefix to match.
095                wsPrefix = Name.EMPTY;
096                }
097            else
098                {
099                uninterestingNodes = tfb.getUninterestingNodes(context, wsPrefix, null, true);
100                exhibitCounts = tfb.getExhibitCount(context, wsPrefix, null);
101                }
102    
103            // 302 TEMP REDIRECTS FOR NODES THAT CURRENTLY DUPLICATE THEIR PARENT NODE.
104            // If we can redirect to a parent node (ie shorter URI) than this
105            // that (currently) contains the same info, then we do,
106            // to show shorter URLs to users and to reduce duplicate pages in SEs/caches.
107            // Since the tree structure may change this must only be a temporary redirect.
108            final boolean redirected = redirectIfCanonicalisationNeeded(tfb, response, baseURI, wsPrefix, context, nodeContent);
109            if(redirected) { return; }
110    
111            // Allow for i18n.
112            final LocaleBean localeBean = new LocaleBean();
113            localeBean.setRequest(request, getServletContext());
114    
115            // Set breadcrumbs back to top of virtual collections...
116            final Breadcrumbs crumbs = getBreadcrumbs(localeBean);
117    
118            // Redirect to display JSP, having stored the data we need in attributes at request level.
119            request.setAttribute(WebConsts.STDHDR_ATTR_NAME_BREADCRUMBS, crumbs);
120            request.setAttribute(WebConsts.BETDJ_BASE_URI_ATTR_NAME, baseURI);
121            request.setAttribute(WebConsts.BETDJ_LIST_ATTR_NAME, nodeContent);
122            request.setAttribute(WebConsts.BETDJ_LIST_UNIN_NAME, uninterestingNodes);
123            request.setAttribute(WebConsts.BETDJ_LIST_SNEXCOUNT_NAME, exhibitCounts);
124            request.setAttribute(WebConsts.BETDJ_PRWSEQ_ATTR_NAME, wsPrefix);
125            request.setAttribute(WebConsts.BETDJ_TITLE_ATTR_NAME,
126                localeBean.getLocalisedMessage(getTitleCommonCatalogueName()));
127            request.setAttribute(WebConsts.BETDJ_HTML_GENERAL_DESC_ATTR_NAME,
128                localeBean.getLocalisedMessage(getDescriptionCommonCatalogueName()));
129    
130            request.getRequestDispatcher(WebConsts.BROWSABLE_EXHIBIT_TREE_DISPLAY_JSP).forward(request, response);
131            }
132    
133        /**302 TEMP REDIRECTS FOR NODES THAT CURRENTLY DUPLICATE THEIR PARENT NODE.
134         * @return true if a redirect has been done,
135         *     so the caller should return immediately
136         *     and not try to generate any page content.
137         */
138        public static boolean redirectIfCanonicalisationNeeded(final TreeFilterBean tfb,
139                                                               final HttpServletResponse response,
140                                                               final String baseURI,
141                                                               final CharSequence wsPrefix,
142                                                               final ServletContext context,
143                                                               final List<CharSequence> nodeContent)
144            throws IOException, IllegalStateException
145            {
146            // 302 TEMP REDIRECTS FOR NODES THAT CURRENTLY DUPLICATE THEIR PARENT NODE.
147            // If we can redirect to a parent node (ie shorter URI) than this
148            // that (currently) contains the same info, then we do,
149            // to show shorter URLs to users and to reduce duplicate pages in SEs/caches.
150            // Since the tree structure may change this must only be a temporary redirect.
151            // We must go as far as we can in one step/redirection
152            // since clients will object to multiple redirections one step at a time.
153            // We do not (yet) attempt to preserve any query strings/parameters, etc...
154            boolean redirected = false;
155            if(WebConsts.CANON_TREE_URIS_WITH_REDIR && (wsPrefix.length() > 1))
156                {
157                // The best (shortest) prefix we can find with the same info; never null.
158                CharSequence bestParentNodePrefix = wsPrefix;
159                while(bestParentNodePrefix.length() > 1)
160                    {
161                    // Include everything up to and including
162                    // the *penultimate* word separator,
163                    // ie avoiding the very final character.
164                    final Name parentPrefix = Name.create(bestParentNodePrefix.subSequence(0,
165                        1 + TextUtils.lastIndexOf(bestParentNodePrefix, ExhibitName.WORD_SEP, bestParentNodePrefix.length()-2)));
166                    assert((parentPrefix.length() == 0) || (TextUtils.endsWith(parentPrefix, ExhibitName.WORD_SEPS))) : parentPrefix;
167                    assert(parentPrefix.length() < bestParentNodePrefix.length());
168                    assert(TextUtils.startsWith(bestParentNodePrefix, parentPrefix));
169    
170                    // If the parent node contains the same info as the current node
171                    // then it becomes our redirection target but we continue looking,
172                    // else we stop since if this node doesn't match then higher ones won't either.
173                    if(nodeContent.equals(tfb.selectPrefixesAndShortNames(context, parentPrefix, null)))
174                        { bestParentNodePrefix = parentPrefix; }
175                    else
176                        { break; }
177                    }
178    
179                // Did we manage to find an equivalent parent node?
180                // Do a temp redirect to it if so.
181                if(bestParentNodePrefix.length() < wsPrefix.length())
182                    {
183                    response.sendRedirect(makeOfficialURI(baseURI, bestParentNodePrefix));
184                    redirected = true;
185                    }
186                }
187            return redirected;
188            }
189    
190        /**This clears the cache in the filter bean.
191         * We chain to super.destroy() to tidy up the underlying generic servlet.
192         */
193        @Override
194        public void destroy()
195            {
196            try
197                {
198                // Free up any now-redundant memory resources quickly.
199                tfb.clearCache();
200                }
201            finally { super.destroy(); }
202            }
203    
204        /**Initialises the servlet the filter bean.
205         * Derived class can choose to override it but should chain to it if so.
206         * <p>
207         * This sets the memory sensitivity and the filter expression.
208         *
209         * @throws javax.servlet.ServletException
210         */
211        @Override public void init() throws ServletException
212            {
213    //        // By default retain cache of computed data as this is assumed to be expensive,
214    //        // but if memory is stressed when this is invoked then adaptively become memory sensitive.
215    //        tfb.setMemorySensitiveCache(MemoryTools.isMemoryStressed());
216    
217            // By default assume that it's better to allow an rebuild than an OOM,
218            // so default to making all these assume (derivative) trees memory-sensitive.
219            tfb.setMemorySensitiveCache(true);
220    
221            // By default, set exactly once, at initialisation.
222            tfb.setExpr(getFilter());
223            }
224    
225        /**Get the filter expression to apply.
226         * Override in derived classes.
227         */
228        protected abstract Expr getFilter();
229    
230        /**Get (short) title text common catalogue name; null if none.
231         * The expanded text should be 7-bit printable ASCII,
232         * possibly with embedded entities and mark-up.
233         * <p>
234         * Override in derived classes.
235         */
236        protected abstract String getTitleCommonCatalogueName();
237    
238        /**Get descriptive text (i18n-ed if possible); null if none.
239         * The expanded text should be 7-bit printable ASCII,
240         * possibly with embedded entities and mark-up.
241         * <p>
242         * Defaults to null (no descriptive text).
243         * <p>
244         * Override as needed in derived classes.
245         */
246        protected String getDescriptionCommonCatalogueName()
247            { return(null); }
248    
249        /**Get Breadcrumbs, empty if none; never null.
250         * By default, just a link to the top of the virtual collections,
251         * but can either be added to or replaced by an overriding class.
252         */
253        protected Breadcrumbs getBreadcrumbs(final LocaleBeanBase localeBean)
254            {
255            if(null == localeBean) { throw new IllegalArgumentException("localeBean"); }
256            final Breadcrumbs crumbs = new Breadcrumbs();
257            crumbs.append(new Breadcrumbs.Breadcrumb(
258                localeBean.getLocalisedMessage("common.main.VirtualCollections"),
259                WebConsts.VIRTUAL_COLLECTIONS_ROOT));
260            return(crumbs);
261            }
262    
263        /**Function to provide full "official" URI for page given base URI and word prefix; never null nor empty.
264         * Note that the URI that this returns always ends with a slash
265         * to unambiguously mark the whole thing as a directory;
266         * no trailing "welcome" file component is added nor should be.
267         * <p>
268         * The suffix is also forced to lower-case to ensure canonicalisation.
269         *
270         * @param baseURI  base URI for this tree; never null
271         * @param prwseq  prefix word sequence, "" or lower-case word sequence
272         *     ending in a hyphen; never null
273         */
274        public static String makeOfficialURI(final CharSequence baseURI, final CharSequence prwseq)
275            {
276            if(null == baseURI) { throw new IllegalArgumentException("baseURI"); }
277            if(null == prwseq) { throw new IllegalArgumentException("prwseq"); }
278            final String result = baseURI + (TextUtils.endsWith(baseURI, "/") ? "" : "/") +
279                prwseq.toString().toLowerCase().replace(ExhibitName.WORD_SEP, '/');
280            assert(TextUtils.startsWith(result, baseURI));
281            assert(result.endsWith("/")); // Always a directory.
282            return(result);
283            }
284        }