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 }