View Javadoc
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 &quot;filename&quot; 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 &quot;file&quot; 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 }