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}