001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.jdbc;
007
008import java.io.*;
009import java.util.*;
010import java.util.regex.*;
011import java.lang.reflect.*;
012import fc.io.*;
013import fc.util.*;
014
015/** 
016Loads sql queries from a file. Storing queries in a file (when
017the number of queries is greater than 10 or so, is a lot more 
018manageable than embeddding these as strings in the program).
019
020<p>
021In the query file:
022<ul style="list-style: square inside url('data:image/gif;base64,R0lGODlhBQAKAIABAAAAAP///yH5BAEAAAEALAAAAAAFAAoAAAIIjI+ZwKwPUQEAOw==')">
023
024<li>comments start with hash or //</li>
025
026<li>// comments can also end a line and anything after a // on a line is ignored.</li>
027
028<li>empty lines (just whitespaces) are ignored</li>
029
030<li>queries are of format:
031<blockquote><pre>
032queryname = querycontent
033  ...querycontent continued...
034  ;
035</pre></blockquote>
036</li>
037<li>a ';' character by itself (on a separate line) ends a name = content
038section. The query is stored and can be retrieved via it's name.
039</li>
040
041<li><p>
042a special section within the content is enclosed within <b>$...$</b>
043and is processed when this file is read. This section (if specified) refers
044to a corresponding molly DBO class. For example:
045</p>
046<blockquote><pre>
047userDetailQuery = 
048  select 
049    <b>$</b>package.foo<b>$</b>, <b>$</b>x.y.bar<b>$</b> from
050  ...rest of query...
051;
052</pre></blockquote>
053In this example, <code>package.foo</code> will be replaced by the call to
054<code>package.<b>fooMgr</b>.columns()</code> and <code>$x.y.bar$</code> will be replaced
055by a call to <code>x.y.<b>barMgr</b>.columns()</code>. Note, the names must be
056fully qualified (that is, including the package name) since otherwise we won't
057be able to locate the corresponding Mgr class at runtime.
058<p>
059The <b>$...$</b> can end with an optional <i>prefix</i>
060after the ending $, for example, $...$<b><span style="color:blue">xxx</span></b>
061<p>
062If present, the <code>package.<b>fooMgr</b>.columns(<span style="color:blue"><i>prefix</i></span>)</code> is invoked to get the column
063names. A prefix other than the default <i>tablename</i>.colname
064formulation, (such as <i>xxx</i>_colname) is necessary, if the queryname
065uses a <i>tablename <b>AS</b> <span style="color:blue">xxx</span></i> abbreviation anywhere 
066in the query (if an abbreviation is used via the SQL AS clause, then that
067abbreviation must be used everywhere, not the tablename).
068</p>
069</li>
070
071<li>to access a constant static final class attribute, use <b>$$...$$</b>. This is useful when constant values (declared in code) are to be used in a query.
072<blockquote><pre>
073userDetailQuery = 
074  select 
075    <b>$$</b>x.y.SOME_VAL<b>$$</b> 
076  ...rest of query...
077;
078</pre></blockquote>
079This will replace the value of the java field/attribute <code>x.y.XYZ</code> (in declaring class <code>y</code> located in package <code>x</code>).
080This method does a straight value interpolation and does not add SQL stringify quotes if they are 
081needed. For String or character types, quotes should be manually placed around the replaced value, 
082such as:
083<blockquote><pre>
084userDetailQuery = 
085  select 
086    <span style="color:red">"</span><b>$$</b>x.y.SOME_STRING_VALUE<b>$$</b><span style="color:red">"</span>
087;
088</pre></blockquote>
089<p>
090Note: the fields accessed this way must be delared as <b>final</b> and <b>static</b>. If they are not, then a error will be thrown when processing the query.
091</li>
092</ul>
093
094<hr>
095
096Here is a real world <i>example</i> of this file in action:
097<p>
098<ol>
099
100<li>
101The following file is called <b>my.queries</b> (the name is arbitrary) and
102stored under <i>WEB-INF/foo/bar</i>
103<pre>
104# Get all friends of X
105#   - get list of friends for X (friend uids from friends table)
106# - get info about those friend uids from the users table
107userFriends = 
108  SELECT 
109  u.name, u.status, u.gender, u.likes, u.pictime, u.hbtime,
110  f.friend_uid, f.is_liked, f.is_unliked,
111  is_friend(u.uid, ?) as twoway
112  FROM
113  users as u, friends as f
114  WHERE
115  f.uid = ? 
116  and u.uid = f.friend_uid
117;
118
119# Get unread messages sent to user X
120# Unread messages are grouped by the the sender id and message count per sender
121#
122unreadMessages = 
123  SELECT
124  from_uid, count(is_retrieved) as count
125  FROM 
126  messages 
127  WHERE 
128  to_uid = ? and is_retrieved = false 
129  GROUP BY 
130  from_uid
131;
132
133# Gets detailed information about place X
134placeDetail = 
135  SELECT
136      $my.dbobj.location$
137  FROM 
138    location 
139  WHERE
140  location_id = ?
141;
142</pre>
143</li>
144
145<li>
146This is then initialized/accessed from a servlet via the following code snippet:
147<pre>
148try 
149  {
150  //queryMgr is an instance variable in the servlet (can be later accessed
151  //from other methods). webroot is a file pointing to the root directory
152  //of this context.
153
154  queryMgr = new QueryReader(
155    new File(webroot, "<b>WEB-INF/foo/bar/my.queries</b>"), log); 
156  log.info("Available queries: ", queryMgr.getQueries().keySet());
157  }
158catch (IOException e) {
159  throw new ServletException(IOUtil.throwableToString(e));
160  }
161</pre>
162</li>
163
164<li>
165Note: the placeDetail query in the example file contains the $..$
166replaceable text <i>$my.dbobj.location$</i>. This means, that the
167<i>my.dbobj.*</i> classes should be included in the invoking servlet's
168classpath, such that the corresponding <i>my.dbobj.locationMgr</i>
169class is found.
170<p>
171This would be ensured by putting the following at the top of the
172servlet code above:
173<p>
174import <i>my.dbobj.*</i>;
175</p>
176</li>
177</ol>
178**/
179public final class QueryReader
180{
181private static final boolean dbg = false;
182public static final int   TEST_FIELD_1 = 1;
183public static final String  TEST_FIELD_2 = "hello";
184
185Log   log;
186Map   queries = new LinkedHashMap();
187
188/** 
189Creates a query reader that reads queries from the specified file,
190using the UTF-8 encoding and a default logger.
191
192@throws IOException   on error reading from the file
193*/
194public QueryReader(File f) throws IOException
195  {
196  this(f, Log.getDefault());
197  }
198  
199/** 
200Creates a query reader that reads queries from the specified file,
201using the UTF-8 encoding and the specified logger.
202
203@throws IOException   on error reading from the file
204*/
205public QueryReader(File f, Log logger) throws IOException
206  {
207  log = logger; 
208  BufferedReader in
209      = new BufferedReader(
210          new InputStreamReader(new FileInputStream(f), "UTF-8"));
211  
212  String line = null;
213  StringBuilder buf = new StringBuilder (1024);
214  
215  while ( (line = in.readLine()) != null)
216    {
217    String trimline = line.trim();  //this gets rid of spaces, empty newlines, etc
218
219    if (trimline.length() == 0 
220      || trimline.startsWith("#") 
221      || trimline.startsWith("//")) 
222      {
223      if (dbg) System.out.println("Skipping: " + line);
224      continue;
225      }
226
227    //this skips a series of lines containing ;
228    if (buf.length() == 0 && trimline.equals(";"))
229      {
230      if (dbg) System.out.println("Skipping: " + line);
231      continue;
232      }
233
234    //ignore trailing comments starting with "//"
235    String[] split = line.split("\\s+//", 2);
236    line = split[0];
237  
238    if (dbg && split.length > 1) {
239      System.out.println("Splitting line with trailing //");
240      System.out.println("split=" + Arrays.asList(split));
241      }
242    
243    if (trimline.equals(";")) 
244      {
245      processBuffer(buf);
246      buf = new StringBuilder();
247      }
248    else if (line.trim().endsWith(";")) 
249      {
250      //Not in spec but added just for safety in case ';' appears 
251      //on the same line 
252      buf.append(line.substring(0, line.lastIndexOf(';')));
253      processBuffer(buf);  
254      buf = new StringBuilder();
255      }
256    else{
257      //append original line, not trimline, this allows us
258      //to keep original leading spaces in the query 
259      buf.append(line);
260      //this is important, either append a newline or a space
261      //otherwise separate lines in the query file run into each
262      //other IF there is no trailing/leading spaces on each
263      //line. newline good since it preserves original formatting
264      buf.append("\n");
265      }
266    }
267    
268  String remaining = buf.toString().trim();
269  if (remaining.length() > 0) {
270    log.error("No ending delimiter (';') seen, the following section was NOT processed: \n", remaining);
271    }
272    
273  in.close();
274  log.info("Processed: ", queries.size(), " queries from file: ", f.getAbsolutePath());
275  }
276
277//process 1 query at a time (invoked when ending ';' for each query is seen)
278void processBuffer(StringBuilder buf)
279  {
280  String[] keyval = buf.toString().split("=", 2);
281
282  if (dbg) {
283    System.out.println(Arrays.asList(keyval));
284    }
285
286  if (keyval.length != 2) {
287    log.error("Query sections must be of form 'name = value'. Badly formed section, NOT processed: \n", buf); 
288    return;
289    }
290    
291  String name = keyval[0].trim();
292  String value = keyval[1].trim();
293
294  if (queries.get(name) != null) {
295    log.error("This query name ", name, " already exists prior to this section. Duplicate name NOT processed: \n", buf);  
296    return;
297    }
298
299  StringBuffer sb = new StringBuffer();
300  
301  //dbo columns pattern
302  //the first [^$] skips the $$ part (for field value expressions). 
303  Pattern p = Pattern.compile("[^$]\\$\\s*([a-zA-Z_0-9.]+)\\s*\\$([a-zA-Z_0-9.]*)");  //=>  \$(...)\$optionalprefix
304  Matcher m = p.matcher(value);
305  while (m.find()) 
306    {
307    if (dbg) System.out.println("Matched columns $regex: " + m.group());
308    String dbo = m.group(1);
309    String mgrName = dbo + "Mgr";
310
311    String prefix = m.group(2);
312      
313    if (dbg) System.out.println("Manager name = " +  mgrName);
314    if (dbg) System.out.println("Column prefix name = " +  prefix);
315    
316    try {
317      Class mgr = Class.forName(mgrName, true,
318        Thread.currentThread().getContextClassLoader());
319    
320      Method method = null;
321      String columns = null;
322    
323      if (prefix != null && ! prefix.equals("")) {
324        method = mgr.getMethod("columns", new Class[] {String.class});
325        columns = (String) method.invoke(null, prefix);
326        }
327      else{
328        method = mgr.getMethod("columns", null);
329        columns = (String) method.invoke(null, null);
330        }
331      
332      m.appendReplacement(sb, columns);
333      if (dbg) System.out.println("replacing: [" + dbo + "] with [" + columns + "]");
334      }
335    catch (ClassNotFoundException e) {
336      log.error("Manager [", mgrName, "] for [$", dbo + "$] not found, this query will NOT be added. Query:\n-----------------------\n", buf, "\n-----------------------\n");
337      return;     
338      }
339    catch (Exception e2) {
340      log.error("Internal error while processing: ", buf);
341      log.error("This query was NOT added");
342      log.error(IOUtil.throwableToString(e2));
343      return;           
344      }
345    } //~while
346    
347  m.appendTail(sb);
348
349  //constant field values pattern $$...$$
350  p = Pattern.compile("\\$\\$\\s*([a-zA-Z_0-9.]+)\\.([a-zA-Z_0-9]+)\\s*\\$\\$");  
351  m = p.matcher(sb.toString());
352  sb = new StringBuffer();
353  
354  while (m.find()) 
355    {
356    if (dbg) System.out.println("Matched constant field $regex: " + m.group());
357    String classname = m.group(1);
358    String fieldname = m.group(2);
359      
360    if (dbg) System.out.println("class name = " +  classname);
361    if (dbg) System.out.println("field name = " +  fieldname);
362    
363    try {
364      Class c = Class.forName(classname, true, Thread.currentThread().getContextClassLoader());
365    
366      Field field = c.getDeclaredField(fieldname);
367      int modifiers = field.getModifiers();
368      if (! Modifier.isStatic(modifiers) || ! Modifier.isFinal(modifiers)) {
369        throw new Exception("Darn! Field: [" + field + "] was not declared static or final. It must be both for this reference to work!");
370        }
371      if (! Modifier.isPublic(modifiers)) {
372        field.setAccessible(true);
373        }
374      
375      Object fieldval = field.get(null);  
376      
377      m.appendReplacement(sb, String.valueOf(fieldval));
378      if (dbg) System.out.println("replacing: [" + field + "] with [" + fieldval + "]");
379      }
380    catch (ClassNotFoundException e) {
381      log.error("Class [", classname, "] not found, this query will NOT be added. Query:\n-----------------------\n", buf, "\n-----------------------\n");
382      return;     
383      }
384    catch (NoSuchFieldException e) {
385      log.error("Field [", fieldname, "] not found, this query will NOT be added. Query:\n-----------------------\n", buf, "\n-----------------------\n");
386      return;     
387      }
388    catch (Exception e2) {
389      log.error("Internal error while processing: ", buf);
390      log.error("This query was NOT added");
391      log.error(IOUtil.throwableToString(e2));
392      return;           
393      }
394    } //~while
395
396  m.appendTail(sb);
397
398  queries.put(name, sb.toString()); 
399  }
400  
401/** 
402returns the query with the specified name or <code>null</code> if the query
403does not exist. 
404*/
405public String getQuery(String name) {
406  return (String) queries.get(name);
407  }
408
409/** 
410returns the entire query map containing all successfully read queries.
411*/
412public Map getQueries() {
413  return queries;
414  }
415
416  
417public static void main (String args[]) throws IOException
418  {
419  Args myargs = new Args(args);
420  String filestr = myargs.getRequired("file");
421  QueryReader qr = new QueryReader(new File(filestr));
422  System.out.println("----------------- processed queries ------------------");
423  System.out.println(qr.queries);
424  }
425}