View Javadoc
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   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     this(null, selectedColumns, otherTables, whereClause, orderByClause);
176   }
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                        String whereClause, String orderByClause) {
192 
193     this.database = selectedColumns[0].getDatabase();
194 
195     // Make a list of all the tables used
196 
197     Vector<Table<?>> tablesV = new Vector<Table<?>>();
198 
199     for (int c = 0; c < selectedColumns.length; ++c) {
200       Table<Persistent> table = selectedColumns[c].getTable();
201       if (!tablesV.contains(table))
202         tablesV.addElement(table);
203     }
204     for (int t = 0; t < otherTables.length; ++t)
205       if (!tablesV.contains(otherTables[t]))
206         tablesV.addElement(otherTables[t]);
207 
208     tables = new Table[tablesV.size()];
209     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     Vector<Column<?>> columnsV = new Vector<Column<?>>();
217     Vector<Integer> canReadColumnIndices = new Vector<Integer>();
218     Vector<Table<?>> tablesWithoutCanReadColumnV = new Vector<Table<?>>();
219 
220     selectedColumnsCount = selectedColumns.length;
221     for (int c = 0; c < selectedColumns.length; ++c) {
222       columnsV.addElement(selectedColumns[c]);
223     }
224     
225     for (int t = 0; t < tables.length; t++) {
226       Table<?> table = tables[t];
227       Column<Capability> canRead = table.canReadColumn();
228       if (canRead == null) {
229         // No specific canRead column, revert to the table default protection
230         if (!tablesWithoutCanReadColumnV.contains(table))
231           tablesWithoutCanReadColumnV.addElement(table);
232       }
233       else {
234         if (!columnsV.contains(canRead)) {
235           canReadColumnIndices.addElement(new Integer(columnsV.size()));
236           columnsV.addElement(canRead);
237         } else { 
238           canReadColumnIndices.addElement(new Integer(columnsV.indexOf(canRead)));          
239         }
240       }
241     }     
242 
243     this.columns = new Column[columnsV.size()];
244     columnsV.copyInto(this.columns);
245 
246     isCanReadColumn = new boolean[columnsV.size()];
247     for (int i = 0; i < canReadColumnIndices.size(); ++i) {
248       int c = ((Integer)canReadColumnIndices.elementAt(i)).intValue();
249       isCanReadColumn[c] = true;
250     }
251 
252     this.tablesWithoutCanReadColumn = new Table[tablesWithoutCanReadColumnV.size()];
253     tablesWithoutCanReadColumnV.copyInto(this.tablesWithoutCanReadColumn);
254 
255     // Make up the SQL for the query
256 
257     StringBuffer sqlLocal = new StringBuffer();
258 
259     sqlLocal.append("SELECT ");
260 
261     if (modifier != null) {
262       sqlLocal.append(modifier);
263       sqlLocal.append(' ');
264     }
265 
266     for (int c = 0; c < columnsV.size(); ++c) {
267       if (c > 0) sqlLocal.append(", ");
268       Column<?> column = (Column<?>)columnsV.elementAt(c);
269       sqlLocal.append(column.getTable().quotedName());
270       sqlLocal.append('.');
271       sqlLocal.append(column.quotedName());
272     }
273 
274     sqlLocal.append(" FROM ");
275 
276     for (int t = 0; t < tables.length; ++t) {
277       if (t > 0) sqlLocal.append(", ");
278       sqlLocal.append((tables[t]).quotedName());
279     }
280 
281     if (whereClause != null && !whereClause.trim().equals("")) {
282       sqlLocal.append(" WHERE ");
283       sqlLocal.append(whereClause);
284     }
285 
286     if (orderByClause != null && !orderByClause.trim().equals("")) {
287       sqlLocal.append(" ORDER BY ");
288       sqlLocal.append(orderByClause);
289     }
290 
291     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     for (int c = 0; c < columnsV.size(); ++c) {
297       Column<?> column = columnsV.elementAt(c);
298       table_columnMap.put(
299           column.getTable().getName() + "_" + column.getName(),
300           new Integer(c));
301     }
302   }
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     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                                                 ResultSet resultSet) {
388       super(query, resultSet);
389     }
390 
391     protected Object mapped(ResultSet them) {
392       checkTableAccess();
393       for (int c = 1; c < query.columns.length; ++c)
394         column(them, c);
395       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     return new FirstRawTailoredResultSetEnumeration<Object>(this,
404                                                     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     return super.toString() + "(SQL=" + sql +  ")";
414   }
415 }