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 }