1 /*
2 * Copyright (C) 2000 Tim Joyce
3 *
4 * Part of Melati (http://melati.org), a framework for the rapid
5 * development of clean, maintainable web applications.
6 *
7 * Melati is free software; Permission is granted to copy, distribute
8 * and/or modify this software under the terms either:
9 *
10 * a) the GNU General Public License as published by the Free Software
11 * Foundation; either version 2 of the License, or (at your option)
12 * any later version,
13 *
14 * or
15 *
16 * b) any version of the Melati Software License, as published
17 * at http://melati.org
18 *
19 * You should have received a copy of the GNU General Public License and
20 * the Melati Software License along with this program;
21 * if not, write to the Free Software Foundation, Inc.,
22 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
23 * GNU General Public License and visit http://melati.org to obtain the
24 * Melati Software License.
25 *
26 * Feel free to contact the Developers of Melati (http://melati.org),
27 * if you would like to work out a different arrangement than the options
28 * outlined here. It is our intention to allow Melati to be used by as
29 * wide an audience as possible.
30 *
31 * This program is distributed in the hope that it will be useful,
32 * but WITHOUT ANY WARRANTY; without even the implied warranty of
33 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34 * GNU General Public License for more details.
35 *
36 * Contact details for copyright holder:
37 *
38 * Tim Joyce <timj At paneris.org>
39 * http://paneris.org/
40 * 68 Sandbanks Rd, Poole, Dorset. BH14 8BY. UK
41 */
42
43 package org.melati.servlet;
44
45 import org.melati.Melati;
46 import org.melati.PoemContext;
47 import org.melati.poem.*;
48 import org.melati.poem.util.StringUtils;
49
50 import javax.servlet.ServletException;
51 import javax.servlet.http.HttpServletRequest;
52 import java.io.IOException;
53
54 /**
55 * Base class to use Poem with Servlets.
56 * <p>
57 * Simply extend this class and override the doPoemRequest method. If you are
58 * going to use a template engine look at TemplateServlet.
59 * <UL>
60 * <LI> <A NAME=pathinfoscan>By default, the path info of the URL by which the
61 * servlet was called up is examined to determine the `logical name' of the
62 * Melati POEM database to which the servlet should connect, and possibly a
63 * table within that database, an object within that table, and a `method' to
64 * apply to that object.</A> The URL is expected to take one of the following
65 * forms: <BLOCKQUOTE><TT> http://<I>h</I>/<I>s</I>/<I>db</I>/ <BR>
66 * http://<I>h</I>/<I>s</I>/<I>db</I>/<I>meth</I> <BR>
67 * http://<I>h</I>/<I>s</I>/<I>db</I>/<I>tbl</I>/<I>meth</I> <BR>
68 * http://<I>h</I>/<I>s</I>/<I>db</I>/<I>tbl</I>/<I>troid</I>/<I>meth</I>
69 * <BR>
70 * http://<I>h</I>/<I>s</I>/<I>db</I>/<I>tbl</I>/<I>troid</I>/<I>meth</I>/<I>other</I>
71 * </TT></BLOCKQUOTE> and the following components are broken out of the path
72 * info and passed to your application code in the <TT>melati</TT> parameter
73 * (which is also copied automatically into <TT>context</TT> so that it is
74 * easily available in templates):
75 * <TABLE>
76 * <caption>Path context elements</caption>
77 * <TR>
78 * <TD><TT><I>h</I></TT></TD>
79 * <TD>host name, such as <TT>www.melati.org</TT></TD>
80 * </TR>
81 * <TR>
82 * <TD><TT><I>s</I></TT></TD>
83 * <TD> servlet-determining part, such as <TT>melati/org.melati.admin.Admin</TT>
84 * </TD>
85 * </TR>
86 * <TR>
87 * <TD><TT><I>db</I></TT></TD>
88 * <TD> The first element of the path info is taken to be the `logical name' of
89 * the Melati POEM database to which the servlet should connect. It is mapped
90 * onto JDBC connection details via the config file <TT>org.melati.LogicalDatabase.properties</TT>,
91 * of which there is an example in the source tree. This is automatically made
92 * available in templates as <TT>$melati.Database</TT>. </TD>
93 * <TR>
94 * <TD><TT><I>tbl</I></TT></TD>
95 * <TD> The DBMS name of a table with which the servlet is concerned: perhaps it
96 * is meant to list its contents. This is automatically made available in
97 * templates as <TT>$melati.Table</TT>. </TD>
98 * </TR>
99 * <TR>
100 * <TD><TT><I>troid</I></TT></TD>
101 * <TD> The POEM `troid' (table row identifier, or row-unique integer) of a row
102 * within <TT><I>tbl</I></TT> with which the servlet is concerned: perhaps it
103 * is meant to display it. This is automatically made available in templates as
104 * <TT>$melati.Object</TT>. </TD>
105 * </TR>
106 * <TR>
107 * <TD><TT><I>meth</I></TT></TD>
108 * <TD> A freeform string telling your servlet what it is meant to do. This is
109 * automatically made available in templates as <TT>$melati.Method</TT>.
110 * </TD>
111 * </TR>
112 * <TR>
113 * <TD><TT><I>other</I></TT></TD>
114 * <TD> Any other information you wish to put in the pathinfo. This is useful,
115 * for instance, if you wish to specify the "filename" of your
116 * servlet. For instance, if you call <TT>/db/myfiles/0/Download/afile.html</TT>
117 * and return a stream with a content-type of <tt>application/octet-stream</tt>
118 * most browsers will prompt you to save the "file" as
119 * <tt>afile.html</tt> </TD>
120 * </TR>
121 * </TABLE>
122 * <LI> You can change the way these things are determined by overriding <TT>poemContext</TT>.
123 * <LI> Any POEM database operations you perform will be done with the access
124 * rights of the POEM <TT>User</TT> associated with the servlet session. If
125 * there is no established servlet session, the current user will be set to the
126 * default `guest' user. If this method terminates with an <TT>AccessPoemException</TT>,
127 * indicating that you have attempted something which you aren't entitled to do,
128 * the user will be prompted to log in, and the original request will be
129 * retried. The precise mechanism used for login is <A
130 * HREF=#loginmechanism>configurable</A>.
131 * <LI>
132 * No changes made to the database by other concurrently executing threads
133 * will be visible to you (in the sense that once you have seen a particular
134 * version of a record, you will always subsequently see the same one), and your
135 * own changes will not be made permanent until this method completes
136 * successfully or you perform an explicit <TT>PoemThread.commit()</TT>. If
137 * it terminates with an exception or you issue a <TT>PoemThread.rollback()</TT>,
138 * your changes will be lost.
139 * <LI> <A NAME=loginmechanism>
140 * It's possible to configure how your <TT>PoemServlet</TT>-derived
141 * servlets implement user login.</A> If the properties file <TT><A
142 * HREF=../org.melati.MelatiConfig.properties>
143 * org.melati.MelatiConfig.properties</A></TT> exists and contains a setting
144 * <TT>org.melati.MelatiConfig.accessHandler=<I>foo</I></TT>, then <TT><I>foo</I></TT>
145 * is taken to be the name of a class implementing the <TT>AccessHandler</TT>
146 * interface. The default is <TT>HttpSessionAccessHandler</TT>, which stores
147 * the user id in the servlet session, and redirects to the <TT>Login</TT>
148 * servlet to throw up templated login screens. If instead you specify
149 * <TT>HttpBasicAuthenticationAccessHandler</TT>, the user id is maintained
150 * using HTTP Basic Authentication (RFC2068 11.1, the
151 * mechanism commonly used to password-protect static pages), and the task of
152 * popping up login dialogs is delegated to the browser. The advantage of the
153 * former method is that the user gets a more informative interface which is
154 * more under the designer's control; the advantage of the latter method is that
155 * no cookies or URL rewriting are required---for instance it is probably more
156 * appropriate for WAP phones. Both methods involve sending the user's password
157 * in plain text across the public network.
158 * </UL>
159 *
160 * @see org.melati.poem.Database#guestAccessToken
161 * @see org.melati.poem.PoemThread#commit
162 * @see org.melati.poem.PoemThread#rollback
163 * @see #poemContext
164 * @see org.melati.login.AccessHandler
165 * @see org.melati.login.HttpSessionAccessHandler
166 * @see org.melati.login.Login
167 * @see org.melati.login.HttpBasicAuthenticationAccessHandler
168 */
169
170 public abstract class PoemServlet extends ConfigServlet {
171
172 /**
173 * Eclipse generated.
174 */
175 private static final long serialVersionUID = 7694978400584943446L;
176
177 /**
178 * A place to do things before entering the session
179 * of the user, here is a good place to use root access token.
180 *
181 * Overriden in TemplateServlet.
182 *
183 * @param melati
184 * org.melati.Melati A source of information about the Melati
185 * database context (database, table, object) and utility objects
186 * such as error handlers.
187 */
188
189 protected void prePoemSession(Melati melati) throws Exception {
190 }
191
192 /**
193 * @see javax.servlet.Servlet#destroy()
194 */
195 public void destroy() {
196 super.destroy();
197 }
198
199 /**
200 * Process the request.
201 */
202
203 protected void doConfiguredRequest(final Melati melati)
204 throws ServletException, IOException {
205
206 // Set up a POEM session and call the application code
207
208 // Do something outside of the PoemSession
209 try {
210 melati.getConfig().getAccessHandler().buildRequest(melati);
211 prePoemSession(melati);
212 } catch (Exception e) {
213 // we have to log this here, otherwise we lose the stacktrace
214 error(melati, e);
215 throw new TrappedException("Problem in prePoemSession", e);
216 }
217
218 final PoemServlet _this = this;
219
220 melati.getDatabase().inSession(AccessToken.root, new PoemTask() {
221 @SuppressWarnings("unchecked")
222 public void run() {
223 String poemAdministratorsName;
224 String poemAdministratorsEmail;
225
226 try {
227 try {
228 poemAdministratorsName = melati.getDatabase().administratorUser().getName();
229 Field<String> emailField;
230 try {
231 emailField = (Field<String>)melati.getDatabase().administratorUser().getField("email");
232 poemAdministratorsEmail = emailField.toString();
233 } catch (NoSuchColumnPoemException e) {
234 poemAdministratorsEmail = "noEmailDefined@nobody.com";
235 }
236 _this.setSysAdminName(poemAdministratorsName);
237 _this.setSysAdminEmail(poemAdministratorsEmail);
238 } catch (Exception e) {
239 _handleException(melati, e);
240 }
241 } catch (Exception e) {
242 // we have to log this here, otherwise we lose the stacktrace
243 error(melati, e);
244 throw new TrappedException(e);
245 }
246
247
248 melati.getConfig().getAccessHandler().establishUser(melati);
249 melati.loadTableAndObject();
250
251 try {
252 try {
253 _this.doPoemRequest(melati);
254 } catch (Exception e) {
255 _handleException(melati, e);
256 }
257 } catch (Exception e) {
258 // we have to log this here, otherwise we lose the stacktrace
259 error(melati, e);
260 throw new TrappedException(e);
261 }
262 }
263
264 public String toString() {
265 HttpServletRequest request = melati.getRequest();
266 return "PoemServlet: "
267 + ((request == null) ? "(no request present)" : request
268 .getRequestURI());
269 }
270 });
271 }
272
273 /**
274 * Override this to provide a different administrator's details to the
275 * database admin user.
276 *
277 * @return the System Administrators name.
278 */
279 public String getSysAdminName() {
280 return sysAdminName;
281 }
282
283 /**
284 * Override this to provide a different administrator's details to the
285 * database admin user.
286 *
287 * @return the System Administrators email address.
288 */
289 public String getSysAdminEmail() {
290 return sysAdminEmail;
291 }
292
293 /**
294 * Default method to handle an exception without a template engine.
295 *
296 * @param melati
297 * the Melati
298 * @param exception
299 * the exception to handle
300 */
301 protected void handleException(Melati melati, Exception exception)
302 throws Exception {
303 if (exception instanceof AccessPoemException) {
304 melati.getConfig().getAccessHandler().handleAccessException(melati,
305 (AccessPoemException) exception);
306 } else {
307 throw exception;
308 }
309 }
310
311 protected final void _handleException(Melati melati, Exception exception)
312 throws Exception {
313 try {
314 handleException(melati, exception);
315 } catch (Exception e) {
316 PoemThread.rollback();
317 throw e;
318 }
319 }
320
321 protected PoemContext poemContext(Melati melati) throws PathInfoException {
322
323 PoemContext it = new PoemContext();
324
325 String initParameterPathInfo = getInitParameter("pathInfo");
326 String[] parts;
327 if (initParameterPathInfo != null)
328 parts = StringUtils.split(initParameterPathInfo, '/');
329 else
330 parts = melati.getPathInfoParts();
331
332 // set it to something in order to provoke meaningful error
333 it.setLogicalDatabase("");
334 if (parts.length > 0) {
335 it.setLogicalDatabase(parts[0]);
336 if (parts.length == 2)
337 it.setMethod(parts[1]);
338 if (parts.length == 3) {
339 it.setTable(parts[1]);
340 it.setMethod(parts[2]);
341 }
342 if (parts.length >= 4) {
343 it.setTable(parts[1]);
344 try {
345 it.setTroid(new Integer(parts[2]));
346 } catch (NumberFormatException e) {
347 throw new PathInfoException(melati.getRequest().getPathInfo(), e);
348 }
349 if (parts.length == 4) {
350 it.setMethod(parts[3]);
351 } else {
352 String pathInfo = melati.getRequest().getPathInfo();
353 pathInfo = pathInfo.substring(1);
354 for (int i = 0; i < 3; i++) {
355 pathInfo = pathInfo.substring(pathInfo.indexOf("/") + 1);
356 }
357 it.setMethod(pathInfo);
358 }
359 }
360 }
361 return it;
362 }
363
364 /*
365 * This is provided for convenience, so you don't have to specify the
366 * logicaldatabase on the pathinfo. This is a very good idea when writing your
367 * applications where you are typically only accessing a single database.
368 * Simply override poemContext(Melati melati) thus:
369 * <code>
370 * protected PoemContext poemContext(Melati melati) throws PathInfoException {
371 * return poemContextWithLDB(melati,"<your logical database name>");
372 * }
373 * </code>
374 */
375 protected PoemContext poemContextWithLDB(Melati melati, String logicalDatabase)
376 throws PathInfoException {
377 PoemContext it = new PoemContext();
378 String initParameterPathInfo = getInitParameter("pathInfo");
379 String[] parts;
380 if (initParameterPathInfo != null)
381 parts = StringUtils.split(initParameterPathInfo, '/');
382 else
383 parts = melati.getPathInfoParts();
384
385 // set it to something in order to provoke meaningful error
386 it.setLogicalDatabase(logicalDatabase);
387 if (parts.length > 0) {
388 if (parts.length == 1)
389 it.setMethod(parts[0]);
390 if (parts.length == 2) {
391 it.setTable(parts[0]);
392 it.setMethod(parts[1]);
393 }
394 if (parts.length >= 3) {
395 it.setTable(parts[0]);
396 it.setMethod(parts[2]);
397 try {
398 it.setTroid(new Integer(parts[1]));
399 } catch (NumberFormatException e) {
400 throw new PathInfoException(melati.getRequest().getPathInfo(), e);
401 }
402 }
403 if (parts.length == 3) {
404 it.setMethod(parts[2]);
405 } else {
406 String pathInfo = melati.getRequest().getPathInfo();
407 pathInfo = pathInfo.substring(1);
408 for (int i = 0; i < 2; i++) {
409 pathInfo = pathInfo.substring(pathInfo.indexOf("/") + 1);
410 }
411 it.setMethod(pathInfo);
412 }
413
414 }
415 return it;
416 }
417
418 /**
419 * Override this method to build up your own output.
420 */
421 protected abstract void doPoemRequest(Melati melati) throws Exception;
422
423 }