Coverage Report - org.melati.poem.TailoredQuery
 
Classes in this File Line Coverage Branch Coverage Complexity
TailoredQuery
100%
70/70
92%
37/40
4
TailoredQuery$FirstRawTailoredResultSetEnumeration
100%
7/7
100%
2/2
4
 
 1  
 /*
 2  
  * $Source$
 3  
  * $Revision$
 4  
  *
 5  
  * Copyright (C) 2000 William Chesters
 6  
  *
 7  
  * Part of Melati (http://melati.org), a framework for the rapid
 8  
  * development of clean, maintainable web applications.
 9  
  *
 10  
  * Melati is free software; Permission is granted to copy, distribute
 11  
  * and/or modify this software under the terms either:
 12  
  *
 13  
  * a) the GNU General Public License as published by the Free Software
 14  
  *    Foundation; either version 2 of the License, or (at your option)
 15  
  *    any later version,
 16  
  *
 17  
  *    or
 18  
  *
 19  
  * b) any version of the Melati Software License, as published
 20  
  *    at http://melati.org
 21  
  *
 22  
  * You should have received a copy of the GNU General Public License and
 23  
  * the Melati Software License along with this program;
 24  
  * if not, write to the Free Software Foundation, Inc.,
 25  
  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 26  
  * GNU General Public License and visit http://melati.org to obtain the
 27  
  * Melati Software License.
 28  
  *
 29  
  * Feel free to contact the Developers of Melati (http://melati.org),
 30  
  * if you would like to work out a different arrangement than the options
 31  
  * outlined here.  It is our intention to allow Melati to be used by as
 32  
  * wide an audience as possible.
 33  
  *
 34  
  * This program is distributed in the hope that it will be useful,
 35  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 36  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 37  
  * GNU General Public License for more details.
 38  
  *
 39  
  * Contact details for copyright holder:
 40  
  *
 41  
  *     William Chesters <williamc At paneris.org>
 42  
  *     http://paneris.org/~williamc
 43  
  *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
 44  
  */
 45  
 
 46  
 package org.melati.poem;
 47  
 
 48  
 import java.util.Enumeration;
 49  
 import java.util.Vector;
 50  
 import java.util.Hashtable;
 51  
 import java.sql.ResultSet;
 52  
 
 53  
 /**
 54  
  * This is how you run low-level SQL queries, including joins, and get the
 55  
  * results back in the form of convenient Melati {@link Field}s which can be
 56  
  * rendered automatically in templates.  A complement to the high-level
 57  
  * {@link Table#selection()} on the one hand, and the low-level
 58  
  * <TT>ResultSet</TT> on the other.
 59  
  *
 60  
  *
 61  
  * <blockquote>
 62  
  * 
 63  
  * TailoredQuery is specifically for when you want a few fields back
 64  
  * (possibly joined from several tables) rather than whole objects.
 65  
  * Suppose you want to do
 66  
  * <blockquote><code>
 67  
  *   SELECT a.foo, b.bar FROM a, b WHERE a.something AND b.id = a.b
 68  
  * </code></blockquote>
 69  
  * There is nothing to stop you doing this with a good old ResultSet =
 70  
  * Database.sqlQuery("SELECT ...").  However if you want to get the same
 71  
  * effect, without forgoing the nice features offered by
 72  
  * POEM---e.g. access control, rich metadata that makes rendering
 73  
  * trivial---you can use a TailoredQuery.
 74  
  *
 75  
  * </blockquote>
 76  
  *
 77  
  * <p>
 78  
  *
 79  
  * If Postgresql's <TT>ResultSetMetaData</TT> supported <TT>getTableName</TT>
 80  
  * even approximately, this would all be "even simpler" to use and somewhat
 81  
  * more flexible.  Because it doesn't, and because of the requirement to
 82  
  * perform (as far as possible) read access checks on the records accessed
 83  
  * during the query, the interface necessarily takes a slightly structured form
 84  
  * rather than just being raw SQL---although the programmer does get complete
 85  
  * freedom as far as the core <TT>WHERE</TT> clause is concerned.
 86  
  *
 87  
  * @see Table#selection(java.lang.String, java.lang.String, boolean)
 88  
  */
 89  
 
 90  
 public class TailoredQuery {
 91  
 
 92  
   protected Database database;
 93  
   protected String sql;
 94  
   int selectedColumnsCount;
 95  
   Column<?>[] columns;
 96  
   boolean[] isCanReadColumn;
 97  
   Table<?>[] tables;
 98  
   Table<?>[] tablesWithoutCanReadColumn;
 99  
 
 100  12
   Hashtable<String, Integer> table_columnMap = new Hashtable<String,Integer>();
 101  
 
 102  
   /**
 103  
    * Construct a low-level SQL query, possibly including joins, from which
 104  
    * results come back in form of automatically-renderable Melati
 105  
    * <TT>Field</TT>s.  The queries you can construct are of the form
 106  
    *
 107  
    * <BLOCKQUOTE><TT>
 108  
    * SELECT <I>t1</I>.<I>c1</I>, <I>t2</I>.<I>c2</I> </TT>...<TT>
 109  
    * FROM <I>t1</I>, <I>t2</I> </TT>...<TT>,
 110  
    *      <I>t10</I>, <I>t11</I> </TT>...<TT>
 111  
    * WHERE <I>whereClause</I> 
 112  
    * ORDER BY <I>orderByClause</I>
 113  
    * </TT></BLOCKQUOTE>
 114  
    *
 115  
    * You specify the columns you want to return
 116  
    * (<TT><I>t1</I>.<I>c1</I></TT>&nbsp;...) in the <TT>selectedColumns</TT>
 117  
    * parameter, the selection criteria (including joins) in the
 118  
    * <TT>whereClause</TT> parameter, and the ordering criteria in the
 119  
    * <TT>orderByClause</TT>.  If your <TT>whereClause</TT> or
 120  
    * <TT>orderByClause</TT> use tables <TT><I>t10</I></TT>&nbsp;... not implied
 121  
    * by <TT>selectedColumns</TT>, you must include them in the
 122  
    * <TT>otherTables</TT> parameter.
 123  
    *
 124  
    * <P>
 125  
    *
 126  
    * Note that there is no provision for aliasing, which does restrict the
 127  
    * available queries somewhat.
 128  
    *
 129  
    * <P>
 130  
    *
 131  
    * To get the results of the query, use <TT>selection()</TT> (below).
 132  
    *
 133  
    * <P>
 134  
    *
 135  
    * Example:
 136  
    * <BLOCKQUOTE><PRE>
 137  
    * Column[] columns = {
 138  
    *     database.getUserTable().getNameColumn(),
 139  
    *     database.getGroupTable().getNameColumn(),
 140  
    * };
 141  
    * Table[] tables = { database.getGroupMembershipTable() };
 142  
    * 
 143  
    * TailoredQuery q =
 144  
    *     new TailoredQuery(
 145  
    *    columns, tables,
 146  
    *    "\"user\" = \"user\".id AND \"group\" = \"group\".id",
 147  
    *    null);
 148  
    * 
 149  
    * for (Enumeration ms = q.selection(); ms.hasMoreElements();) {
 150  
    *   FieldSet fs = (FieldSet)ms.nextElement();
 151  
    *   System.out.println(fs.get("user_name").getCookedString(PoemLocale.HERE, DateFormat.MEDIUM) +
 152  
    *                 ", " +
 153  
    *                 fs.get("group_name").getCookedString(PoemLocale.HERE, DateFormat.MEDIUM));
 154  
    * }
 155  
    * </PRE></BLOCKQUOTE>
 156  
    *
 157  
    * @param selectedColumns     The columns you want to select out
 158  
    * @param otherTables         Tables aside from those to which your
 159  
    *                            <TT>selectedColumns</TT> are attached which
 160  
    *                            you need to use in the <TT>whereClause</TT>
 161  
    * @param whereClause         Search criteria for your query; note that
 162  
    *                            you will have to do any necessary quoting of
 163  
    *                            identifiers/values yourself (or use
 164  
    *                            <TT>Column.quotedName</TT> and
 165  
    *                            <TT>PoemType.quotedRaw</TT>)
 166  
    * @param orderByClause       Ordering criteria for your query
 167  
    *
 168  
    * @see #selection
 169  
    * @see Column#quotedName()
 170  
    * @see BasePoemType#quotedRaw(java.lang.Object)
 171  
    */
 172  
 
 173  
   public TailoredQuery(Column<?>[] selectedColumns, Table<?>[] otherTables,
 174  
                        String whereClause, String orderByClause) {
 175  5
     this(null, selectedColumns, otherTables, whereClause, orderByClause);
 176  5
   }
 177  
 
 178  
   /**
 179  
    * Same as without the first argument except that it is inserted 
 180  
    * between <code>SELECT</code> and the column list.
 181  
    *
 182  
    * @param modifier  HACK Allow SQL modifier eg DISTINCT 
 183  
    * @param selectedColumns
 184  
    * @param otherTables
 185  
    * @param whereClause
 186  
    * @param orderByClause
 187  
    * @see #TailoredQuery(Column[], Table[], String, String)
 188  
    */
 189  
   public TailoredQuery(String modifier,  
 190  
                        Column<?>[] selectedColumns, Table<?>[] otherTables,
 191  12
                        String whereClause, String orderByClause) {
 192  
 
 193  12
     this.database = selectedColumns[0].getDatabase();
 194  
 
 195  
     // Make a list of all the tables used
 196  
 
 197  12
     Vector<Table<?>> tablesV = new Vector<Table<?>>();
 198  
 
 199  34
     for (int c = 0; c < selectedColumns.length; ++c) {
 200  22
       Table<Persistent> table = selectedColumns[c].getTable();
 201  22
       if (!tablesV.contains(table))
 202  19
         tablesV.addElement(table);
 203  
     }
 204  23
     for (int t = 0; t < otherTables.length; ++t)
 205  11
       if (!tablesV.contains(otherTables[t]))
 206  5
         tablesV.addElement(otherTables[t]);
 207  
 
 208  12
     tables = new Table[tablesV.size()];
 209  12
     tablesV.copyInto(tables);
 210  
 
 211  
     // Record the access protection sources for all the tables used (of course
 212  
     // this doesn't include computed `Persistent.assertCanRead's written by the
 213  
     // programmer).  Make up a list of all the columns we need, included any
 214  
     // `canRead' access control columns for tables.
 215  
 
 216  12
     Vector<Column<?>> columnsV = new Vector<Column<?>>();
 217  12
     Vector<Integer> canReadColumnIndices = new Vector<Integer>();
 218  12
     Vector<Table<?>> tablesWithoutCanReadColumnV = new Vector<Table<?>>();
 219  
 
 220  12
     selectedColumnsCount = selectedColumns.length;
 221  34
     for (int c = 0; c < selectedColumns.length; ++c) {
 222  22
       columnsV.addElement(selectedColumns[c]);
 223  
     }
 224  
     
 225  36
     for (int t = 0; t < tables.length; t++) {
 226  24
       Table<?> table = tables[t];
 227  24
       Column<Capability> canRead = table.canReadColumn();
 228  24
       if (canRead == null) {
 229  
         // No specific canRead column, revert to the table default protection
 230  21
         if (!tablesWithoutCanReadColumnV.contains(table))
 231  21
           tablesWithoutCanReadColumnV.addElement(table);
 232  
       }
 233  
       else {
 234  3
         if (!columnsV.contains(canRead)) {
 235  2
           canReadColumnIndices.addElement(new Integer(columnsV.size()));
 236  2
           columnsV.addElement(canRead);
 237  
         } else { 
 238  1
           canReadColumnIndices.addElement(new Integer(columnsV.indexOf(canRead)));          
 239  
         }
 240  
       }
 241  
     }     
 242  
 
 243  12
     this.columns = new Column[columnsV.size()];
 244  12
     columnsV.copyInto(this.columns);
 245  
 
 246  12
     isCanReadColumn = new boolean[columnsV.size()];
 247  15
     for (int i = 0; i < canReadColumnIndices.size(); ++i) {
 248  3
       int c = ((Integer)canReadColumnIndices.elementAt(i)).intValue();
 249  3
       isCanReadColumn[c] = true;
 250  
     }
 251  
 
 252  12
     this.tablesWithoutCanReadColumn = new Table[tablesWithoutCanReadColumnV.size()];
 253  12
     tablesWithoutCanReadColumnV.copyInto(this.tablesWithoutCanReadColumn);
 254  
 
 255  
     // Make up the SQL for the query
 256  
 
 257  12
     StringBuffer sqlLocal = new StringBuffer();
 258  
 
 259  12
     sqlLocal.append("SELECT ");
 260  
 
 261  12
     if (modifier != null) {
 262  3
       sqlLocal.append(modifier);
 263  3
       sqlLocal.append(' ');
 264  
     }
 265  
 
 266  36
     for (int c = 0; c < columnsV.size(); ++c) {
 267  24
       if (c > 0) sqlLocal.append(", ");
 268  24
       Column<?> column = (Column<?>)columnsV.elementAt(c);
 269  24
       sqlLocal.append(column.getTable().quotedName());
 270  24
       sqlLocal.append('.');
 271  24
       sqlLocal.append(column.quotedName());
 272  
     }
 273  
 
 274  12
     sqlLocal.append(" FROM ");
 275  
 
 276  36
     for (int t = 0; t < tables.length; ++t) {
 277  24
       if (t > 0) sqlLocal.append(", ");
 278  24
       sqlLocal.append((tables[t]).quotedName());
 279  
     }
 280  
 
 281  12
     if (whereClause != null && !whereClause.trim().equals("")) {
 282  9
       sqlLocal.append(" WHERE ");
 283  9
       sqlLocal.append(whereClause);
 284  
     }
 285  
 
 286  12
     if (orderByClause != null && !orderByClause.trim().equals("")) {
 287  1
       sqlLocal.append(" ORDER BY ");
 288  1
       sqlLocal.append(orderByClause);
 289  
     }
 290  
 
 291  12
     this.sql = sqlLocal.toString();
 292  
 
 293  
     // Set up mappings from column name (<table>_<column>) to position
 294  
     // (including the canRead columns, if anyone ever wants them)
 295  
 
 296  36
     for (int c = 0; c < columnsV.size(); ++c) {
 297  24
       Column<?> column = columnsV.elementAt(c);
 298  48
       table_columnMap.put(
 299  24
           column.getTable().getName() + "_" + column.getName(),
 300  
           new Integer(c));
 301  
     }
 302  12
   }
 303  
 
 304  
   /**
 305  
    * Run the query.
 306  
    *
 307  
    *
 308  
    * <P>
 309  
    *
 310  
    * Here's an example of presenting the results of a <TT>TailoredQuery</TT> in
 311  
    * a WebMacro template:
 312  
    *
 313  
    * <BLOCKQUOTE><TT>
 314  
    * &lt;TABLE&gt;<BR>
 315  
    * &nbsp;#foreach $fieldSet in $tailoredQuery.selection() #begin<BR>
 316  
    * &nbsp;&nbsp;&lt;TR&gt;<BR>
 317  
    * &nbsp;&nbsp;&nbsp;#foreach $field in $fieldSet #begin<BR>
 318  
    * &nbsp;&nbsp;&nbsp;&nbsp;&lt;TD&gt;$ml.rendered($field.DisplayName)&lt;/TD&gt;<BR>
 319  
    * &nbsp;&nbsp;&nbsp;&nbsp;&lt;TD&gt;$ml.rendered($field)&lt;/TD&gt;<BR>
 320  
    * &nbsp;&nbsp;&nbsp;#end<BR>
 321  
    * &nbsp;&nbsp;&lt;/TR&gt;<BR>
 322  
    * &nbsp;#end<BR>
 323  
    * &lt;/TABLE&gt;
 324  
    * </TT></BLOCKQUOTE>
 325  
    *
 326  
    * <P>
 327  
    *
 328  
    * Read access checks are performed against the POEM access token associated
 329  
    * with the thread running this method (see
 330  
    * <TT>PoemThread.accessToken()</TT>) on all the tables implied by the
 331  
    * <TT>selectedColumns</TT> and <TT>otherTables</TT> arguments given at
 332  
    * construct time.  If the checks fail for a given row, all the fields in the
 333  
    * corresponding <TT>FieldSet</TT> are booby-trapped to throw the relevant
 334  
    * <TT>AccessPoemException</TT> (<I>all</I> of them, because,
 335  
    * without parsing your <TT>whereClause</TT>, there's no way for POEM to
 336  
    * know which
 337  
    * columns are `tainted'; note that it's probably possible for you to bypass
 338  
    * access checks by using sub-SELECTs).
 339  
    *
 340  
    * <P>
 341  
    *
 342  
    * Normally, Melati's response to an "access-denied" fields is to terminate
 343  
    * template expansion and ask the user to log in, then continue where they
 344  
    * were left off.  If, however, you put 
 345  
    * <TT>$melati.setPassbackExceptionHandling()</TT> 
 346  
    * at the top of the template, or in the servlet, 
 347  
    * they will be rendered as warnings by <I>e.g.</I> <TT>HTMLMarkupLanguage</TT>.
 348  
    *
 349  
    * <P>
 350  
    *
 351  
    * NOTE Since this way of doing queries involves named fields rather
 352  
    * than whole <TT>Persistent</TT> objects, it inevitably bypasses any
 353  
    * Java-coded access rules put in place by the programmer by overriding
 354  
    * <TT>Persistent.assertCanRead</TT>.
 355  
    *
 356  
    * @see FieldSet#elements()
 357  
    * @see FieldSet#get(java.lang.String)
 358  
    * @see PoemThread#accessToken()
 359  
    * @see Persistent#assertCanRead(org.melati.poem.AccessToken)
 360  
    * Used in org.melati.template.MarkupLanguage#rendered(Object)
 361  
    *
 362  
    * @return 
 363  
    * An <TT>Enumeration</TT> of <TT>FieldSet</TT>s, one per row returned from
 364  
    * the DBMS.  You can invoke each <TT>FieldSet</TT>'s <TT>elements</TT>
 365  
    * method to get an <TT>Enumeration</TT> of all the <TT>Field</TT>s in the
 366  
    * row, ready for rendering.  Or you can fetch them by name using the
 367  
    * <TT>FieldSet</TT>'s <TT>get</TT> method.  A field corresponding to
 368  
    * column <TT><I>col</I></TT> of table <TT><I>tab</I></TT> is named
 369  
    * <TT><I>tab</I>_<I>col</I></TT>.
 370  
    */
 371  
 
 372  
   public Enumeration<FieldSet> selection() {
 373  5
     return new TailoredResultSetEnumeration<FieldSet>(this, database.sqlQuery(sql));
 374  
   }
 375  
 
 376  
   /**
 377  
    * Return an Enumeration of the columns in the first row of 
 378  
    * a ResultSet.
 379  
    */
 380  
   public class FirstRawTailoredResultSetEnumeration<T>
 381  
       extends TailoredResultSetEnumeration<T> {
 382  
 
 383  
    /**
 384  
     * Retrieve the first row of a {@link TailoredQuery}.
 385  
     */
 386  
     public FirstRawTailoredResultSetEnumeration(TailoredQuery query,
 387  2
                                                 ResultSet resultSet) {
 388  2
       super(query, resultSet);
 389  2
     }
 390  
 
 391  
     protected Object mapped(ResultSet them) {
 392  4
       checkTableAccess();
 393  5
       for (int c = 1; c < query.columns.length; ++c)
 394  1
         column(them, c);
 395  4
       return column(them, 0);
 396  
     }
 397  
   }
 398  
 
 399  
   /**
 400  
    * @return the first row of this <code>TailoredQuery</code>
 401  
    */
 402  
   public Enumeration<Object> selection_firstRaw() {
 403  1
     return new FirstRawTailoredResultSetEnumeration<Object>(this,
 404  1
                                                     database.sqlQuery(sql));
 405  
   }
 406  
   
 407  
   /**
 408  
    * The SQL of the query.
 409  
    * {@inheritDoc}
 410  
    * @see java.lang.Object#toString()
 411  
    */
 412  
   public String toString() {
 413  7
     return super.toString() + "(SQL=" + sql +  ")";
 414  
   }
 415  
 }