KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > derbyBuild > ODBCMetadataGenerator


1 /*
2
3    Derby - Class org.apache.derby.catalog.ODBCProcedureColsVTI
4
5    Licensed to the Apache Software Foundation (ASF) under one or more
6    contributor license agreements. See the NOTICE file distributed with
7    this work for additional information regarding copyright ownership.
8    The ASF licenses this file to You under the Apache License, Version 2.0
9    (the "License"); you may not use this file except in compliance with
10    the License. You may obtain a copy of the License at
11
12       http://www.apache.org/licenses/LICENSE-2.0
13
14    Unless required by applicable law or agreed to in writing, software
15    distributed under the License is distributed on an "AS IS" BASIS,
16    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17    See the License for the specific language governing permissions and
18    limitations under the License.
19
20  */

21
22 package org.apache.derbyBuild;
23
24 import java.io.IOException JavaDoc;
25 import java.io.InputStream JavaDoc;
26 import java.io.InputStreamReader JavaDoc;
27 import java.io.LineNumberReader JavaDoc;
28 import java.io.FileWriter JavaDoc;
29
30 import java.util.Properties JavaDoc;
31 import java.util.HashMap JavaDoc;
32 import java.util.ArrayList JavaDoc;
33
34 import org.apache.derby.iapi.services.sanity.SanityManager;
35
36 /* ****
37  * This class is used at COMPILE TIME ONLY. It is responsible for generating
38  * ODBC metadata queries based on existing JDBC queries. In a word,
39  * this class reads from the org/apache/derby/impl/jdbc/metadata.properties
40  * file (which is where the JDBC queries are stored), and for each query,
41  * performs the changes/additions required to make it comply with ODBC
42  * standards. The generated ODBC queries are written to an output file
43  * that is then used, at build time, to create a full set of both JDBC and
44  * ODBC queries, all of which are then loaded into the database system
45  * tables at creation time.
46  *
47  * For more on the ODBC specification of the metadata methods in question,
48  * see:
49  *
50  * "http://msdn.microsoft.com/library/default.asp?url=/library/en-us/odbc/
51  * htm/odbcsqlprocedures.asp"
52  *
53  * For more on how the generated queries are used at execution time, see
54  * EmbedDatabaseMetadata.java and SystemProcedures.java in the codeline.
55  *
56  */

57 public class ODBCMetadataGenerator {
58
59     // Types of changes that are possible. There are three
60
// types that we handle here:
61
//
62
// 1. Column rename:
63
// Rename a column to have an ODBC-specified name.
64
// For ex. change "SCALE" to "DECIMAL_DIGITS"
65
// 2. Type and/or value change:
66
// Cast a column to an OBDC-specified type. At time
67
// of writing, this was just for casting INTs to
68
// SMALLINTs; OR modify an existing JDBC value
69
// to match the ODBC specification.
70
// 3. Additional column(s):
71
// Add a new, ODBC-specified column to an existing
72
// result set.
73
private final byte COL_RENAME_CHANGE = 0x01;
74     private final byte TYPE_VALUE_CHANGE = 0x02;
75     private final byte ADD_COLUMN_CHANGE = 0x04;
76
77     // Notice written before each generated ODBC statement.
78
private final String JavaDoc ODBC_QUERY_NOTICE =
79         "#\n# *** NOTE! *** The following query was generated\n" +
80         "# AUTOMATICALLY at build time based on the existing\n" +
81         "# JDBC version of the query. DO NOT MODIFY this\n" +
82         "# generated query by hand. Instead, modify either\n" +
83         "# 1) the JDBC version of the query in the codeline \n" +
84         "# file \"metadata.properties\" (which will then get\n" +
85         "# propagated at build time), 2) the relevant SQL\n" +
86         "# fragments in 'odbcgen_fragments.properties' in\n" +
87         "# the codleine, or 3) the ODBCMetadataGenerator\n" +
88         "# class in the org/apache/derbyBuild directory.\n";
89
90     // Prefix to append to all ODBC queries. NOTE: if you change
91
// this value, you'll have to modify EmbedDatabaseMetadata.java
92
// to reflect the change.
93
private final String JavaDoc ODBC_QUERY_PREFIX = "odbc_";
94
95     // Name to use when making JDBC queries into subqueries
96
// (loaded from odbcFragments). NOTE: if you change this value,
97
// you'll have to modify "odbcgen_fragments.properties" to
98
// reflect the change.
99
private final String JavaDoc SUBQUERY_NAME = "JDBC_SUBQUERY";
100
101     // Mock value used to accomplish insertion of new columns.
102
private final String JavaDoc NEW_COL_PLACEHOLDER = "COLUMN_POSITION_HOLDER";
103
104     // Used for trimming 'whitespace'.
105
private final short FOLLOWING = 1;
106     private final short PRECEDING = -1;
107
108     // List of what types of changes are required for a given
109
// metadata procedure.
110
private HashMap JavaDoc changeMap;
111
112     // SQL fragments and keywords that are used in composing
113
// ODBC metadata queries. These are loaded from a file
114
// once and then used throughout the generation process
115
// to build the ODBC queries piece-by-piece.
116
private Properties JavaDoc odbcFragments;
117
118     // Output file; all processed statements are written to this
119
// file. At BUILD TIME, this file will clobber the copy of
120
// "metadata.properties" that is in the BUILD/CLASSES
121
// directory. NOTE: this will NOT clobber the metadata
122
// properties file that is in the SOURCE/CODELINE.
123
private FileWriter JavaDoc odbcMetaFile;
124
125     /* ****
126      * Constructor.
127      * Initializes SQL fragments used for generation, and
128      * then opens the output file,
129      */

130     public ODBCMetadataGenerator() throws IOException JavaDoc {
131
132         // SQL fragments.
133
odbcFragments = new Properties JavaDoc();
134         odbcFragments.load(this.getClass().getResourceAsStream(
135             "odbcgen_fragments.properties"));
136
137         // Prep output file.
138
odbcMetaFile = new FileWriter JavaDoc("odbc_metadata.properties");
139
140     }
141
142     /* ****
143      * main:
144      * Open the metadata.properties file (the copy that is in the
145      * build directory, NOT the one in the source directory),
146      * figure out what changes are needed for the various metadata
147      * queries, and then generate the ODBC-compliant versions
148      * where needed.
149      * @param args Ignored.
150      * @return ODBC-compliant metadata statements have been
151      * generated and written out to "odbc_metadata.properties"
152      * in the running directory.
153      */

154     public static void main(String JavaDoc [] args) throws IOException JavaDoc {
155
156         ODBCMetadataGenerator odbcGen = new ODBCMetadataGenerator();
157         odbcGen.initChanges();
158         odbcGen.generateODBCQueries(odbcGen.getClass().getResourceAsStream(
159             "/org/apache/derby/impl/jdbc/metadata.properties"));
160
161     }
162
163     /* ****
164      * initChanges
165      * Create a listing of the types of changes that need to be
166      * made for each metadata query to be ODBC-compliant.
167      * If a metadata query has no entry in this map, then
168      * it is left unchanged and no ODBC-version will be created.
169      * Having this mapping allows us to skip over String
170      * parsing (which can be slow) when it's not required.
171      * For details on the changes, see the appropriate methods
172      * below.
173      * @return Map holding the list of changes to be made for
174      * each metadata query has been initialized.
175      */

176     private void initChanges() {
177
178         changeMap = new HashMap JavaDoc();
179
180         changeMap.put("getProcedures",
181             new Byte JavaDoc(COL_RENAME_CHANGE));
182
183         changeMap.put("getProcedureColumns",
184             new Byte JavaDoc((byte)(COL_RENAME_CHANGE
185                 | TYPE_VALUE_CHANGE
186                 | ADD_COLUMN_CHANGE)));
187
188         changeMap.put("getColumns",
189             new Byte JavaDoc(TYPE_VALUE_CHANGE));
190
191         changeMap.put("getVersionColumns",
192             new Byte JavaDoc(TYPE_VALUE_CHANGE));
193
194         changeMap.put("getBestRowIdentifierPrimaryKeyColumns",
195             new Byte JavaDoc(TYPE_VALUE_CHANGE));
196
197         changeMap.put("getBestRowIdentifierUniqueKeyColumns",
198             new Byte JavaDoc(TYPE_VALUE_CHANGE));
199
200         changeMap.put("getBestRowIdentifierUniqueIndexColumns",
201             new Byte JavaDoc(TYPE_VALUE_CHANGE));
202
203         changeMap.put("getBestRowIdentifierAllColumns",
204             new Byte JavaDoc(TYPE_VALUE_CHANGE));
205
206         changeMap.put("getTypeInfo",
207             new Byte JavaDoc((byte)(COL_RENAME_CHANGE
208                 | TYPE_VALUE_CHANGE
209                 | ADD_COLUMN_CHANGE)));
210
211         changeMap.put("getIndexInfo",
212             new Byte JavaDoc(TYPE_VALUE_CHANGE));
213
214         return;
215
216     }
217
218     /* ****
219      * generateODBCQueries:
220      * Reads the existing (JDBC) metadata queries from
221      * metadata.properties and, for each one, makes a call
222      * to generate an ODBC-compliant version.
223      * @param is InputStream for reading metadata.properties.
224      */

225     public void generateODBCQueries(InputStream JavaDoc is)
226         throws IOException JavaDoc
227     {
228
229         // JDBC query that we read from metadata.properties.
230
StringBuffer JavaDoc query = new StringBuffer JavaDoc();
231
232         // Note: We use ISO-8859-1 because property files are
233
// defined to be that encoding.
234
LineNumberReader JavaDoc reader =
235             new LineNumberReader JavaDoc(new InputStreamReader JavaDoc(is, "ISO-8859-1"));
236
237         String JavaDoc line = null;
238         for (line = reader.readLine(); line != null;
239             line = reader.readLine())
240         {
241
242             if (line.length() == 0)
243             // blank line; ignore
244
continue;
245             else if (line.charAt(0) == '#') {
246             // comment; write it to file.
247
odbcMetaFile.write(line);
248                 odbcMetaFile.write("\n");
249                 continue;
250             }
251
252             // Write the line, then add an end-of-line to maintain
253
// readability.
254
query.append(line);
255             query.append("\n");
256
257             // Check to see if this is the last line of the query.
258
boolean done = true;
259             for (int lastNonWS = line.length() - 1;
260                 lastNonWS >= 0; lastNonWS--)
261             {
262                 char ch = line.charAt(lastNonWS);
263                 if (!Character.isWhitespace(ch)) {
264                 // this is the last non-whitespace character; if it's
265
// a backslash, then we continue building the query
266
// by reading the next line.
267
if (ch == '\\') {
268                     // then continue building the query.
269
done = false;
270                     }
271                     break;
272                 }
273             }
274
275             if (!done)
276             // read next line and append it to current query.
277
continue;
278
279             // Take the query and see if we need to generate an ODBC-
280
// compliant version.
281
generateODBCQuery(query);
282
283             // Prep for another query.
284
query.delete(0, query.length());
285
286         }
287
288         // Make sure we didn't end up with an incomplete query somewhere.
289
if (query.length() > 0) {
290             throw new IOException JavaDoc(
291                 "Encountered non-terminated query while reading metadata file.");
292         }
293
294         // Close out.
295
odbcMetaFile.flush();
296         odbcMetaFile.close();
297
298     }
299
300     /* ****
301      * generateODBCQuery
302      * Takes a specific JDBC query, writes it to the output file,
303      * and then creates an ODBC-compliant version of that
304      * query (if needed) and writes that to the output file,
305      * as well.
306      * @param queryText SQL text from a JDBC metadata query
307      * that was read from metadata.properties.
308      */

309     private void generateODBCQuery(StringBuffer JavaDoc queryText)
310         throws IOException JavaDoc
311     {
312
313         // Create a string for purposes of using "indexOf"
314
// calls, which aren't allowed on a StringBuffer
315
// for JDBC 2.0.
316
String JavaDoc queryAsString = queryText.toString().trim();
317
318         if (queryAsString.startsWith(ODBC_QUERY_PREFIX))
319         // this query was automatically generated (presumably
320
// by this class), so ignore it now.
321
return;
322
323         // Write the original (JDBC) query.
324
odbcMetaFile.write(queryAsString, 0, queryAsString.length());
325         odbcMetaFile.write("\n\n");
326
327         // Parse out the name of this particular query.
328
int pos = queryAsString.indexOf("=");
329         if (pos == -1) {
330             throw new IOException JavaDoc(
331                 "Failed to extract query name from a JDBC metadata query.");
332         }
333         String JavaDoc queryName = queryText.substring(0, pos);
334
335         // Parse out the ORDER BY clause since they are not allowed
336
// in subqueries; we'll re-attach it later.
337
String JavaDoc orderBy = "";
338         int orderByPos = queryAsString.lastIndexOf("ORDER BY");
339         if (orderByPos != -1)
340             orderBy = queryAsString.substring(orderByPos, queryAsString.length());
341
342         // Isolate query text (remove ORDER BY clause and then query name,
343
// in that order).
344
if (orderByPos != -1)
345             queryText.delete(orderByPos, queryText.length());
346         queryText.delete(0, pos+1);
347
348         // Three types of modifications that we may need to do.
349

350         // -- #1: Column renaming.
351
StringBuffer JavaDoc outerQueryText = new StringBuffer JavaDoc();
352         boolean haveODBCChanges = renameColsForODBC(queryName, queryText);
353
354         // Get a list of the column definitions in the subquery, for
355
// use by subsequent operations.
356
ArrayList JavaDoc colDefs = new ArrayList JavaDoc();
357         pos = getSelectColDefinitions(queryText, colDefs);
358
359         // In some cases, we need to add "helper" columns to the
360
// subquery so that we can use them in calculations for
361
// the outer query.
362
addHelperColsToSubquery(queryName, queryText, pos);
363
364         // -- #2.A: Prep to add new ODBC columns. Note: we need
365
// to do this BEFORE we generate the outer SELECT statement.
366
markNewColPosition(queryName, colDefs);
367
368         // If we're going to use a subquery, generate the outer
369
// SELECT statement. This is where we enforce column
370
// types (via CAST) if needed.
371
generateSELECTClause(queryName, colDefs, outerQueryText);
372
373         // -- #3: Alter column values, where needed.
374
changeValuesForODBC(queryName, outerQueryText);
375
376         // -- #2.B: Add new ODBC columns.
377
addNewColumnsForODBC(queryName, outerQueryText);
378
379         haveODBCChanges = (haveODBCChanges || (outerQueryText.length() > 0));
380         if (!haveODBCChanges)
381         // we didn't change anything, so nothing left to do.
382
return;
383
384         // Write out the new, ODBC version of the query.
385

386         odbcMetaFile.write(ODBC_QUERY_NOTICE);
387         odbcMetaFile.write(ODBC_QUERY_PREFIX);
388         odbcMetaFile.write(queryName);
389         odbcMetaFile.write("=");
390
391         if (outerQueryText.length() == 0) {
392         // all we did was change column names, so just write out the
393
// original query with the new column names.
394
odbcMetaFile.write(queryText.toString());
395             odbcMetaFile.write("\n\n");
396             return;
397         }
398
399         // Else, we need to make the original query a subquery so that we
400
// can change types/values and/or add columns.
401
queryAsString = queryText.toString().trim();
402         odbcMetaFile.write(outerQueryText.toString());
403         odbcMetaFile.write(queryAsString);
404         if (queryText.charAt(queryAsString.length()-1) == '\\')
405             odbcMetaFile.write("\n\\\n) ");
406         else
407             odbcMetaFile.write(" \\\n\\\n) ");
408         odbcMetaFile.write(SUBQUERY_NAME);
409         if (orderBy.length() == 0)
410             odbcMetaFile.write("\n");
411         else {
412         // re-attach ORDER BY clause.
413
odbcMetaFile.write(" \\\n");
414             odbcMetaFile.write(orderBy);
415         }
416         odbcMetaFile.write("\n\n");
417         return;
418
419     }
420
421     /* ****
422      * renameColsForODBC
423      * Renames any columns in the received query so that they are
424      * ODBC-compliant.
425      * @param queryName Name of the query being processed.
426      * @param queryText Text of the query being processed.
427      * @return All columns requiring renaming have been renamed IN
428      * PLACE in the received StringBuffer. True is returned if
429      * at least one column was renamed; false otherwise.
430      */

431     private boolean renameColsForODBC(String JavaDoc queryName, StringBuffer JavaDoc queryText) {
432
433         // If we know the received query doesn't have any columns to
434
// be renamed, then there's nothing to do here.
435
if (!stmtNeedsChange(queryName, COL_RENAME_CHANGE))
436             return false;
437
438         // Which columns are renamed, and what the new names are,
439
// depends on which query we're processing.
440

441         if (queryName.equals("getProcedures")) {
442             renameColForODBC(queryText, "RESERVED1", "NUM_INPUT_PARAMS");
443             renameColForODBC(queryText, "RESERVED2", "NUM_OUTPUT_PARAMS");
444             renameColForODBC(queryText, "RESERVED3", "NUM_RESULT_SETS");
445             return true;
446         }
447         else if (queryName.equals("getProcedureColumns")) {
448             renameColForODBC(queryText, "PRECISION", "COLUMN_SIZE");
449             renameColForODBC(queryText, "LENGTH", "BUFFER_LENGTH");
450             renameColForODBC(queryText, "SCALE", "DECIMAL_DIGITS");
451             renameColForODBC(queryText, "RADIX", "NUM_PREC_RADIX");
452             return true;
453         }
454         else if (queryName.equals("getTypeInfo")) {
455             renameColForODBC(queryText, "PRECISION", "COLUMN_SIZE");
456             renameColForODBC(queryText, "AUTO_INCREMENT", "AUTO_UNIQUE_VAL");
457             return true;
458         }
459
460         // No renaming was necessary.
461
return false;
462
463     }
464
465     /* ****
466      * renameColForODBC
467      * Searches for the old column name in the received String
468      * buffer and replaces it with the new column name. Note
469      * that we only replace the old column name where it is
470      * preceded by "AS", because this is the instance that
471      * determines the column name in the final result set.
472      * @param queryText The query text in which we're doing the
473      * rename operation.
474      * @param oldVal The old column name.
475      * @param newVal The new column name.
476      * @return Occurence of <"AS " + oldVal> in the query text
477      * has been changed IN PLACE to newVal.
478      */

479     private void renameColForODBC(StringBuffer JavaDoc queryText,
480         String JavaDoc oldVal, String JavaDoc newVal)
481     {
482
483         String JavaDoc queryString = queryText.toString();
484         int pos = queryString.indexOf(oldVal);
485         while (pos != -1) {
486
487             // Next line will set pos2 to be the index of the
488
// first (reading left-to-right) ignorable char
489
// preceding the old column name. That means
490
// that the letters immediately preceding this
491
// position should be "AS". If not, don't
492
// replace this instance.
493
int pos2 = trimIgnorable(PRECEDING, queryString, pos);
494             if (((pos2 - 2) > 0) && (queryString.charAt(pos2-2) == 'A')
495                 && (queryString.charAt(pos2-1) == 'S'))
496             { // then this is the one we want to replace.
497
break;
498             }
499             else {
500             // look for next occurrence.
501
pos = queryString.indexOf(oldVal, pos+1);
502             }
503
504         }
505
506         if (pos == -1) {
507         // couldn't find the one to replace; leave unchanged.
508
return;
509         }
510
511         // Do the renaming.
512
queryText.replace(pos, pos + oldVal.length(), newVal);
513
514     }
515
516     /* ****
517      * generateSELECTClause
518      * Generates an outer SELECT clause that is then wrapped around a
519      * JDBC query to change the types and/or values of the JDBC
520      * result set. The JDBC query thus becomes a subquery.
521      *
522      * Ex. if we have a JDBC query "SELECT A, B FROM T1" and ODBC
523      * requires that "A" be a smallint, this method will generate
524      * a select clause "SELECT CAST (T2.A AS SMALLINT), T2.B FROM"
525      * that is then used to wrap the JDBC query, as follows:
526      *
527      * SELECT CAST (T2.A AS SMALLINT), T2.B FROM
528      * (SELECT A, B FROM T1) T2
529      *
530      * @param queryName Name of the query being processed.
531      * @param selectColDefs Array list of the SELECT columns that
532      * exist for the JDBC version of the query. For the above
533      * example, this would be an array list with two String
534      * elements, "A" and "B".
535      * @param newQueryText StringBuffer to which the generated
536      * outer SELECT will be appended.
537      * @return An outer SELECT clause has been generated and
538      * appended to the received buffer. The "FROM" keyword
539      * has been appended, but the subquery itself is NOT
540      * added here.
541      */

542     private void generateSELECTClause(String JavaDoc queryName,
543         ArrayList JavaDoc selectColDefs, StringBuffer JavaDoc newQueryText)
544     {
545
546         if (!stmtNeedsChange(queryName, TYPE_VALUE_CHANGE) &&
547             !stmtNeedsChange(queryName, ADD_COLUMN_CHANGE))
548         { // then we don't need to generate a SELECT, because we
549
// don't need to use a subquery (we're only renaming).
550
return;
551         }
552
553         // Begin the SELECT clause.
554
newQueryText.append("SELECT \\\n\\\n");
555
556         // For each of the SELECT columns in JDBC, either
557
// just grab the column name and use it directly in
558
// the generated clause, or else cast the column
559
// to the required type, if appropriate.
560
String JavaDoc colName;
561         String JavaDoc castInfo;
562         for (int i = 0; i < selectColDefs.size(); i++) {
563             if (i > 0)
564                 newQueryText.append(", \\\n");
565             colName = extractColName((String JavaDoc)selectColDefs.get(i));
566             castInfo = getCastInfoForCol(queryName, colName);
567             if (castInfo != null)
568                 newQueryText.append("CAST (");
569             newQueryText.append(SUBQUERY_NAME);
570             newQueryText.append(".");
571             newQueryText.append(colName);
572             if (castInfo != null) {
573                 newQueryText.append(" AS ");
574                 newQueryText.append(castInfo);
575                 newQueryText.append(")");
576             }
577             if (!colName.equals(NEW_COL_PLACEHOLDER)) {
578             // don't append the "AS" clause if this is just our
579
// place-holder for adding new columns.
580
newQueryText.append(" AS ");
581                 newQueryText.append(colName);
582             }
583         }
584
585         if (newQueryText.charAt(newQueryText.length() - 1) != '\\')
586             newQueryText.append(" \\");
587
588         // End the SELECT clause.
589
newQueryText.append("\nFROM ( ");
590         return;
591
592     }
593
594     /* ****
595      * changeValuesForODBC
596      * Searches for a JDBC column name in the received String
597      * buffer and replaces the first occurrence with an ODBC-
598      * compliant value. This method determines what specific
599      * columns need updated values for a given query, and then
600      * makes the appropriate call for each column.
601      * @param queryName Name of the query being processed.
602      * @param newQueryText The query text in which we're doing the
603      * change-value operation.
604      * @return All relevant columns have been updated IN PLACE
605      * to return the required ODBC-compliant values.
606      */

607     private void changeValuesForODBC(String JavaDoc queryName,
608         StringBuffer JavaDoc newQueryText)
609     {
610
611         if (!stmtNeedsChange(queryName, TYPE_VALUE_CHANGE))
612             return;
613
614         // Which column values are changed, and what the new
615
// values are, depends on which query we're processing.
616

617         if (queryName.equals("getColumns")) {
618             changeColValueToODBC(queryName, "BUFFER_LENGTH", newQueryText);
619             changeColValueToODBC(queryName, "DECIMAL_DIGITS", newQueryText);
620             changeColValueToODBC(queryName, "NUM_PREC_RADIX", newQueryText);
621             changeColValueToODBC(queryName, "SQL_DATA_TYPE", newQueryText);
622             changeColValueToODBC(queryName, "SQL_DATETIME_SUB", newQueryText);
623             changeColValueToODBC(queryName, "CHAR_OCTET_LENGTH", newQueryText);
624         }
625         else if (queryName.startsWith("getBestRowIdentifier")) {
626             changeColValueToODBC(queryName, "BUFFER_LENGTH", newQueryText);
627             changeColValueToODBC(queryName, "DECIMAL_DIGITS", newQueryText);
628         }
629         else if (queryName.equals("getTypeInfo")) {
630             changeColValueToODBC(queryName, "NUM_PREC_RADIX", newQueryText);
631             changeColValueToODBC(queryName, "SQL_DATA_TYPE", newQueryText);
632             changeColValueToODBC(queryName, "SQL_DATETIME_SUB", newQueryText);
633             changeColValueToODBC(queryName, "UNSIGNED_ATTRIBUTE", newQueryText);
634             changeColValueToODBC(queryName, "AUTO_UNIQUE_VAL", newQueryText);
635         }
636         else if (queryName.equals("getProcedureColumns")) {
637             changeColValueToODBC(queryName, "NUM_PREC_RADIX", newQueryText);
638             changeColValueToODBC(queryName, "DECIMAL_DIGITS", newQueryText);
639         }
640
641     }
642
643     /* ****
644      * changeColValueToODBC
645      * Searches for the received column name in the received String
646      * buffer and replaces it with an ODBC-compliant value.
647      * @param queryName Name of the query being processed.
648      * @param colName Name of the specific column to update.
649      * @param newQueryText The query text in which we're doing
650      * the change-value operation.
651      * @return The received column has been updated IN PLACE
652      * to return the required ODBC-compliant value.
653      */

654     private void changeColValueToODBC(String JavaDoc queryName, String JavaDoc colName,
655         StringBuffer JavaDoc newQueryText)
656     {
657
658         colName = SUBQUERY_NAME + "." + colName;
659         int pos = newQueryText.toString().indexOf(colName);
660         if (pos == -1)
661         // column we're supposed to change isn't in the query.
662
return;
663
664         if (colName.endsWith("CHAR_OCTET_LENGTH")) {
665             newQueryText.replace(pos, pos + colName.length(),
666                 getFragment("CHAR_OCTET_FOR_ODBC"));
667         }
668         else if (colName.endsWith("BUFFER_LENGTH")) {
669             newQueryText.replace(pos, pos + colName.length(),
670                 getFragment("BUFFER_LEN_FOR_ODBC"));
671         }
672         else if (colName.endsWith("SQL_DATA_TYPE")) {
673             newQueryText.replace(pos, pos + colName.length(),
674                 getFragment("SQL_DATA_TYPE_FOR_ODBC"));
675         }
676         else if (colName.endsWith("SQL_DATETIME_SUB")) {
677             newQueryText.replace(pos, pos + colName.length(),
678                 getFragment("DATETIME_SUB_FOR_ODBC"));
679         }
680         else if (colName.endsWith("UNSIGNED_ATTRIBUTE")) {
681             newQueryText.replace(pos, pos + colName.length(),
682                 getFragment("UNSIGNED_ATTR_FOR_ODBC"));
683         }
684         else if (colName.endsWith("AUTO_UNIQUE_VAL")) {
685             newQueryText.replace(pos, pos + colName.length(),
686                 getFragment("AUTO_UNIQUE_FOR_ODBC"));
687         }
688         else if (colName.endsWith("DECIMAL_DIGITS")) {
689             newQueryText.replace(pos, pos + colName.length(),
690                 getFragment("DECIMAL_DIGITS_FOR_ODBC"));
691         }
692         else if (colName.endsWith("NUM_PREC_RADIX")) {
693             newQueryText.replace(pos, pos + colName.length(),
694                 getFragment("RADIX_FOR_ODBC"));
695         }
696         else if (colName.endsWith(NEW_COL_PLACEHOLDER)) {
697         // This is a special case indication that we need to add new columns.
698
if (queryName.equals("getProcedureColumns")) {
699                 newQueryText.replace(pos, pos + colName.length(),
700                     getFragment("GET_PROC_COLS_NEW_COLS"));
701             }
702             else if (queryName.equals("getTypeInfo")) {
703                 newQueryText.replace(pos, pos + colName.length(),
704                     getFragment("GET_TYPE_INFO_NEW_COLS"));
705             }
706         }
707
708     }
709
710     /* ****
711      * getSelectColDefinitions
712      * Parses the SELECT clause of a JDBC metadata SQL query
713      * and returns a list of the columns being selected. For
714      * example, if the received statement was "SELECT A,
715      * B AS C, D * 2 FROM T1", this method will return an
716      * ArrayList with three string elements: 1) "A", 2) "B
717      * AS C", and 3) "D * 2".
718      * @param query The query from which we are extracting
719      * the SELECT columns.
720      * @param colDefList ArrayList in which we want to
721      * store the column definitions that we find.
722      * @return Received ArrayList has one string value for
723      * each of the columns found in the received query.
724      * Also, an integer is returned indicating the index
725      * in the received query of the start of the FROM
726      * clause, for later use by the calling method.
727      */

728     private int getSelectColDefinitions(StringBuffer JavaDoc queryText,
729         ArrayList JavaDoc colDefList)
730     {
731
732         // Create a string for purposes of using "indexOf"
733
// calls, which aren't allowed on a StringBuffer
734
// for JDBC 2.0.
735
String JavaDoc query = queryText.toString().trim();
736         char [] queryChars = query.toCharArray();
737
738         // Move beyond the "SELECT" keyword, if there is one.
739
int start = query.indexOf("SELECT");
740         if (start != -1)
741         // "+6" in the next line is length of "SELECT".
742
start += 6;
743         else
744         // just start at the first character.
745
start = 0;
746
747         // Have to read character-by-character in order to
748
// figure out where each column description ends.
749
int fromClauseIndex = -1;
750         int parenDepth = 0;
751         for (int i = start; i < queryChars.length; i++) {
752
753             if (queryChars[i] == '(')
754                 parenDepth++;
755             else if (queryChars[i] == ')')
756                 parenDepth--;
757             else if ((queryChars[i] == ',') && (parenDepth == 0)) {
758             // this is a naive way of determining the end of a
759
// column definition (it'll work so long as there are no
760
// string constants in the query that have commas in them,
761
// which was true at the time of writing.
762
colDefList.add(new String JavaDoc(queryChars, start, (i - start)).trim());
763                 // Skip over non-important whitespace to find start
764
// of next column definition. Next line will set i to
765
// just before the next non-whitespace character.
766
i = trimIgnorable(FOLLOWING, queryChars, i);
767                 start = i + 1;
768             }
769             else if (((i+3) < queryChars.length)
770                 && (parenDepth == 0)
771                 && (queryChars[i] == 'F')
772                 && (queryChars[i+1] == 'R')
773                 && (queryChars[i+2] == 'O')
774                 && (queryChars[i+3] == 'M'))
775             { // this is the end of final column definition; store it
776
// and then exit the loop, after trimming off non-important
777
// whitespace. Next line will set i to just after the
778
// last (reading left-to-right) non-whitespace character
779
// before the FROM.
780
i = trimIgnorable(PRECEDING, queryChars, i);
781                 fromClauseIndex = i;
782                 colDefList.add(new String JavaDoc(queryChars, start, (i - start)).trim());
783                 break;
784
785             }
786
787         }
788
789         return fromClauseIndex;
790
791     }
792
793     /* ****
794      * addHelperColsToSubquery
795      * For some of the metadata queries, the ODBC version
796      * needs to access values that are only available in
797      * the JDBC subquery. In such cases, we want to add
798      * those values as additional "helper" columns to
799      * the subquery result set, so that they can be
800      * referenced from the new ODBC outer query (without
801      * requiring a join). For example, assume we have 2
802      * tables T1(int i, int j) and T2 (int a), and a
803      * subquery "SELECT T1.i, T1.j + T2.a from T1, T2)".
804      * Then we have an outer query that, instead of
805      * returning "T1.j + T2.a", needs to return the
806      * value of "2 * T2.a":
807      *
808      * SELECT VT.i, 2 * T2.a FROM
809      * (SELECT T1.i, T1.j + T2.a FROM T1, T2) VT
810      *
811      * The above statement WON'T work, because the outer
812      * query can't see the value "T2.a". So in such a
813      * a case, this method will add "T2.a" to the list
814      * of columns returned by the subquery, so that the
815      * outer query can then access it:
816      *
817      * SELECT VT.i, 2 * VT.a FROM
818      * (SELECT T1.i, T1.j + T2.a, T2.a FROM T1, T2) VT
819      *
820      * Which specific columns are added to the subquery
821      * depends on the query in question.
822      *
823      * @param queryName Name of the query in question.
824      * @param subqueryText text of the subquery in question.
825      * @param insertPos Index into the received buffer
826      * marking the position where the helper columns
827      * should be inserted.
828      */

829     private void addHelperColsToSubquery(String JavaDoc queryName,
830         StringBuffer JavaDoc subqueryText, int insertPos)
831     {
832
833         if (queryName.equals("getColumns")) {
834             subqueryText.insert(insertPos,
835                 getFragment("GET_COLS_HELPER_COLS"));
836         }
837         else if (queryName.startsWith("getBestRowIdentifier")) {
838             subqueryText.insert(insertPos,
839                 getFragment("BEST_ROW_ID_HELPER_COLS"));
840         }
841
842     }
843
844     /* ****
845      * extractColName
846      * Takes a single column definition from a SELECT clause
847      * and returns only the unqualified name of the column.
848      * Assumption here is that any column definition we see
849      * here will either 1) end with an "AS <COLUMN_NAME>"
850      * clause, or 2) consist of ONLY a column name, such
851      * as "A" or "A.B". At the time of writing, these
852      * assumptions were true for all relevant metadata
853      * queries.
854      *
855      * Ex. If colDef is "A", this method will return "A".
856      * If colDef is "A.B", this method will return "B".
857      * If colDef is "<bunch of SQL> AS C", this method
858      * will return "C".
859      *
860      * @param colDef Column definition from which we're
861      * trying to extract the name.
862      * @return Name of the column that is referenced in
863      * the received column definition.
864      */

865     private String JavaDoc extractColName(String JavaDoc colDef) {
866
867         // Find out where the column name starts.
868
int pos = colDef.lastIndexOf("AS ");
869         if (pos == -1) {
870         // we assume that the col def is _just_ a column name,
871
// so start at the beginning.
872
pos = 0;
873         }
874         else {
875             // Move beyond the "AS".
876
pos += 2;
877
878             // Skip any non-important whitespace or backslashes.
879
char c = colDef.charAt(pos);
880             while ((c == '\\') || Character.isWhitespace(c))
881                 c = colDef.charAt(++pos);
882         }
883
884         // Check to see if it's a qualified name.
885
int pos2 = colDef.indexOf(".", pos);
886         if (pos2 == -1)
887         // it's not a qualified name, so just return it.
888
return colDef.substring(pos, colDef.length());
889
890         // Else, strip off the schema and just return the col name.
891
return colDef.substring(pos2+1, colDef.length());
892
893     }
894
895     /* ****
896      * getCastInfoForCol
897      * Returns the target type for a result set column that
898      * needs to be cast into an ODBC type. This is usually
899      * for casting integers to "SMALLINT".
900      * @param queryName Name of query being processed.
901      * @param colName Name of the specific column for which
902      * we are trying to find the target type.
903      * @return The target type if one exists, or else null
904      * if the received column in the received query has
905      * no known target type.
906      */

907     private String JavaDoc getCastInfoForCol(String JavaDoc queryName,
908         String JavaDoc colName)
909     {
910
911         if (queryName.equals("getTypeInfo")) {
912             if (colName.equals("DATA_TYPE") ||
913                 colName.equals("CASE_SENSITIVE") ||
914                 colName.equals("UNSIGNED_ATTRIBUTE") ||
915                 colName.equals("FIXED_PREC_SCALE") ||
916                 colName.equals("AUTO_UNIQUE_VAL") ||
917                 colName.equals("SQL_DATA_TYPE") ||
918                 colName.equals("SQL_DATETIME_SUB"))
919             {
920                 return "SMALLINT";
921             }
922         }
923         else if (queryName.equals("getColumns")) {
924             if (colName.equals("DECIMAL_DIGITS") ||
925                 colName.equals("NULLABLE") ||
926                 colName.equals("DATA_TYPE") ||
927                 colName.equals("NUM_PREC_RADIX") ||
928                 colName.equals("SQL_DATA_TYPE") ||
929                 colName.equals("SQL_DATETIME_SUB"))
930             {
931                 return "SMALLINT";
932             }
933         }
934         else if (queryName.equals("getProcedureColumns")) {
935             if (colName.equals("DATA_TYPE")) {
936                 return "SMALLINT";
937             }
938         }
939         else if (queryName.equals("getVersionColumns")) {
940             if (colName.equals("DATA_TYPE")) {
941                 return "SMALLINT";
942             }
943         }
944         else if (queryName.startsWith("getBestRowIdentifier")) {
945             if (colName.equals("DATA_TYPE")) {
946                 return "SMALLINT";
947             }
948         }
949         else if (queryName.equals("getIndexInfo")) {
950             if (colName.equals("NON_UNIQUE") ||
951                 colName.equals("TYPE"))
952             {
953                 return "SMALLINT";
954             }
955         }
956
957         // No target type for the received column
958
// in the received query (leave it unchanged).
959
return null;
960
961     }
962
963     /* ****
964      * markNewColPosition
965      * In effect, "marks" the position at which additional
966      * columns are to be added for ODBC compliance. This
967      * is accomplished by adding a dummy column name to
968      * the list of SELECT columns. Later, in the method
969      * that actually adds the columns, we'll do a find-
970      * replace on this dummy value.
971      * @param queryName Name of the query.
972      * @param selectColDefs Array list of the SELECT
973      * columns that exist in the ODBC version of
974      * the query thus far.
975      * @return A dummy column name has been added to
976      * the received list of columns at the position
977      * at which new ODBC columns should be added.
978      * If a query doesn't require additional
979      * columns to be ODBC compliant, this method
980      * leaves the received column list unchanged.
981      */

982     private void markNewColPosition(String JavaDoc queryName,
983         ArrayList JavaDoc selectColDefs)
984     {
985
986         if (!stmtNeedsChange(queryName, ADD_COLUMN_CHANGE))
987             return;
988
989         if (queryName.equals("getProcedureColumns")) {
990         // Add the new columns in front of the Derby-specific ones.
991
// The "-2" in the next line is because there are 2 Derby-
992
// specific columns in the JDBC version of getProcedureCols
993
// (PARAMETER_ID and METHOD_ID).
994
selectColDefs.add(selectColDefs.size() - 2, NEW_COL_PLACEHOLDER);
995         }
996         else if (queryName.equals("getTypeInfo")) {
997         // just add the new column to the end.
998
selectColDefs.add(NEW_COL_PLACEHOLDER);
999         }
1000
1001    }
1002
1003    /* ****
1004     * addNewColumnsForODBC
1005     * Adds new columns to the ODBC version of a metadata
1006     * query (the ODBC version is at this point being
1007     * built up in newQueryText). Before this method
1008     * was called, a dummy placeholder should have been
1009     * placed in the newQueryText buffer (by a call to
1010     * "markNewColPosition"). This method simply replaces
1011     * that dummy placeholder with the SQL text for the
1012     * new columns.
1013     * @param queryName Name of query being processed.
1014     * @newQueryText The buffer in which we want to
1015     * add the new column.
1016     * @return The dummy placeholder in the received
1017     * buffer has been replaced with any ODBC columns
1018     * that need to be added to the query in question
1019     * for ODBC compliance.
1020     */

1021    private void addNewColumnsForODBC(String JavaDoc queryName,
1022        StringBuffer JavaDoc newQueryText)
1023    {
1024
1025        if (!stmtNeedsChange(queryName, ADD_COLUMN_CHANGE))
1026            return;
1027
1028        changeColValueToODBC(queryName, NEW_COL_PLACEHOLDER, newQueryText);
1029
1030        // It's possible that the new column fragments we added
1031
// have placeholders in them for _other_ fragments. We
1032
// need to do the substitution here.
1033
if (queryName.equals("getProcedureColumns")) {
1034            fragSubstitution("SQL_DATA_TYPE_FOR_ODBC", newQueryText);
1035            fragSubstitution("DATETIME_SUB_FOR_ODBC", newQueryText);
1036        }
1037
1038        return;
1039
1040    }
1041
1042    /* ****
1043     * fragSubstitution
1044     * Replaces a single occurrence of the received
1045     * fragment key with the text corresponding to
1046     * that key.
1047     * @param fragKey The fragment key for which we are
1048     * going to do the substitution.
1049     * @queryText The buffer in which we are going to do
1050     * the substitution.
1051     * @return fragKey has been substituted (IN PLACE)
1052     * with the fragment corresponding to it in the
1053     * received buffer. If the fragment key could not
1054     * be found, the buffer remains unchanged.
1055     */

1056    private void fragSubstitution(String JavaDoc fragKey,
1057        StringBuffer JavaDoc queryText)
1058    {
1059
1060        int pos = queryText.toString().indexOf(fragKey);
1061        if (pos != -1) {
1062            // NOTE: the " + 1" and " - 1" in the next line
1063
// are needed because the fragment key is
1064
// enclosed within curly braces ("{}").
1065
queryText.replace(pos - 1, pos + fragKey.length() + 1,
1066                getFragment(fragKey));
1067        }
1068
1069    }
1070
1071    /* ****
1072     * trimIgnorable
1073     * Removes all 'ignorable' chars that immediately precede or
1074     * follow (depending on the direction) the character at
1075     * the received index. "Ignorable" here means whitespace
1076     * OR a single backslash ("\"), which is used in the
1077     * metadata.properties file to indicate line continuation.
1078     * @param direction +1 if we want to trim following, -1
1079     * if we want to trim preceding.
1080     * @param chars The character array being processed.
1081     * @param index The point before/after which to start
1082     * trimming.
1083     * @return The index into the received char array of the
1084     * "last" ignorable character w.r.t the received index
1085     * and direction. In other words, if we're trimming
1086     * the chars FOLLOWING, the returned index will be of
1087     * the last (reading left-to-right) ignorable char; if
1088     * we're trimming the chars PRECEDING, the returned index
1089     * will be of the first (reading left-to-right) ignorable
1090     * character.
1091     */

1092    private int trimIgnorable(short direction, char [] chars, int index) {
1093
1094        index += direction;
1095        while ((index >= 0) && (index < chars.length) &&
1096            ((chars[index] == '\\') ||
1097            Character.isWhitespace(chars[index])))
1098        {
1099            index += direction;
1100        }
1101
1102        // index is now on the final non-ignorable character
1103
// in the given direction. Move it back one so that
1104
// it's on the "last" ignorable character (with
1105
// respect to direction).
1106
index -= direction;
1107
1108        return index;
1109
1110    }
1111
1112    /* ****
1113     * trimIgnorable
1114     * Same as trimIgnorable above, except with String argument
1115     * instead of char[].
1116     */

1117    private int trimIgnorable(short direction, String JavaDoc str, int index) {
1118
1119        index += direction;
1120        while ((index >= 0) && (index < str.length()) &&
1121            ((str.charAt(index) == '\\') ||
1122            Character.isWhitespace(str.charAt(index))))
1123        {
1124            index += direction;
1125        }
1126
1127        // index is now on the final non-ignorable character
1128
// in the given direction. Move it back one so that
1129
// it's on the "first" ignorable character (with
1130
// respect to direction).
1131
index -= direction;
1132
1133        return index;
1134
1135    }
1136
1137    /* ****
1138     * stmtNeedsChange
1139     * Returns whether or not a specific metadata statement
1140     * requires the received type of change. This is determined
1141     * based on the info stored in the "changeMaps" mapping.
1142     * @param queryName Name of the query in question.
1143     * @param changeType The type of change in question.
1144     */

1145    private boolean stmtNeedsChange(String JavaDoc queryName, byte changeType) {
1146
1147        Byte JavaDoc changeByte = (Byte JavaDoc)changeMap.get(queryName);
1148        if (changeByte == null)
1149        // No entry means change is not needed.
1150
return false;
1151
1152        return ((changeByte.byteValue() & changeType) == changeType);
1153
1154    }
1155
1156    /* ****
1157     * getFragment
1158     * Looks up an SQL fragment and returns the value as a String.
1159     * @param String fragId id of the fragment to look up.
1160     * @return The string fragment corresponding to the received
1161     * fragment id.
1162     */

1163    private String JavaDoc getFragment(String JavaDoc fragId) {
1164        return (String JavaDoc)(odbcFragments.get(fragId));
1165    }
1166    
1167}
1168
Popular Tags