EMMA Coverage Report (generated Sat Nov 03 21:53:04 GMT 2007)
[all classes][sf.qof.parser]

COVERAGE SUMMARY FOR SOURCE FILE [SqlParser.java]

nameclass, %method, %block, %line, %
SqlParser.java100% (1/1)100% (12/12)99%  (1020/1028)100% (202.8/203)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class SqlParser100% (1/1)100% (12/12)99%  (1020/1028)100% (202.8/203)
parse (String, boolean): String 100% (1/1)98%  (376/384)100% (57.8/58)
<static initializer> 100% (1/1)100% (7/7)100% (2/2)
SqlParser (String, boolean): void 100% (1/1)100% (44/44)100% (11/11)
extractStrings (String, List): String 100% (1/1)100% (108/108)100% (29/29)
findCurlyBrackets (String): void 100% (1/1)100% (51/51)100% (16/16)
getParameterDefinitions (): ParameterDefinition [] 100% (1/1)100% (9/9)100% (1/1)
getResultDefinitions (): ResultDefinition [] 100% (1/1)100% (9/9)100% (1/1)
getSql (): String 100% (1/1)100% (3/3)100% (1/1)
mergeStrings (String, List): String 100% (1/1)100% (37/37)100% (8/8)
parseDefinition (String, String, StringBuffer, boolean, boolean, int): void 100% (1/1)100% (103/103)100% (22/22)
parseParameterDefinition (String, int): ParameterDefinition 100% (1/1)100% (124/124)100% (24/24)
parseResultDefinition (String, String, int, int): ResultDefinition 100% (1/1)100% (149/149)100% (30/30)

1/*
2 * Copyright 2007 brunella ltd
3 *
4 * Licensed under the LGPL Version 3 (the "License");
5 * you may not use this file except in compliance with the License.
6 *
7 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
8 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
9 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
10 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
11 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
12 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
13 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
14 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
15 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
16 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
17 * THE POSSIBILITY OF SUCH DAMAGE.
18 */
19package sf.qof.parser;
20 
21import java.util.ArrayList;
22import java.util.List;
23import java.util.regex.Matcher;
24import java.util.regex.Pattern;
25 
26import sf.qof.exception.SqlParserException;
27 
28 
29/**
30 * A parser to extract parameter and result definitions. 
31 *
32 * <p> The <code>SqlParser</code> is used to extract parameter and result defintions embedded
33 * in the SQL statement.
34 * 
35 * <p> Parameter definitions have the following form:
36 * <p> <code>{%#}</code> or <code>{type %#}</code>
37 * or <code>{%#.field}</code> or <code>{type %#.field}</code>
38 * <p> <code>#</code> is the index of the Java parameter (1..n) 
39 * <br> <code>field</code> is the name of the field in the mapped Java Bean object 
40 * <br> <code>type</code> is the optional name of mapper (<code>int</code>, <code>string</code>, etc)  
41 * 
42 * <p> Result definitions have the following form:
43 * <p> <code>{%%}</code> or <code>{type %%}</code>
44 * or <code>{%%.field}</code> or <code>{type %%.field}</code> or <code>{%%*}</code>
45 * <p> <code>field</code> is the name of the field in the mapped Java Bean object 
46 * <br> <code>type</code> is the optional name of mapper (<code>int</code>, <code>string</code>, etc)
47 * <br> <code>{%%*}</code> denotes the map key if the return result type is <code>Map</code>
48 *
49 * <p> In/out definitions for stored procedure calls have the following form:
50 * <p> <code>{result definition,parameter definition}</code> or <code>{parameter definition,result definition}</code>
51 * <p> i.e. <code>{%%,%1}</code>, <code>{int %%.id,int %2}</code> 
52 *
53 * <p>For partial definitions add <code>@#</code> at the end of the definition where <code>#</code> is the part number. 
54 * Partial definitions are used for custom mapping adapters when more than one column or parameter maps to the 
55 * same Java object. If more than one definition with the same type are in the same SQL statement then a group
56 * name in brackets is required at the end of the definition <code>[group]</code>. 
57 *
58 * <p> Examples:
59 * <p> <blockquote><pre>
60 * select id {int %%.id,%%*}, name {string %%.name} from person where id = {int %1}
61 * 
62 * insert into person (id, name) values ({%1}, {%2})
63 * 
64 * update person set name = {%2} where id = {%1}
65 * 
66 * delete from person where id = {%1}
67 * 
68 * { %% = call numberOfPersons({%1}) }
69 * 
70 * { call inout({%1,%%}) }
71 * 
72 * insert into person (id, first_name, last_name) values ({%1}, {name%2@1}, {name%2@2})
73 * 
74 * select width {measurement%%.width@1[width]}, unit_of_measurement_width {measurement%%.width@2[width]}, 
75 *   height {measurement%%.height@1[height]}, unit_of_measurement_height {measurement%%.height@2[height]}, 
76 *   length {measurement%%.length@1[length]}, unit_of_measurement_length {measurement%%.length@2[length]} 
77 * from product 
78 * </pre></blockquote>
79 */
80public class SqlParser {
81 
82  private List<ParameterDefinition> parameterDefs;
83  private List<ResultDefinition> resultDefs;
84  private List<Integer> openCurlyBrackets;
85  private List<Integer> closeCurlyBrackets;
86  private int sqlLength;
87  private String sql;
88  private int sqlIndex = 0;
89 
90  /**
91   * Instantiate and parse a SQL statement.
92   * 
93   * @param sql                 a SQL statement
94   * @param isCallableStatement true if the SQL statement is a store procedure call
95   * 
96   * @throws sf.qof.exception.SqlParserException
97   */
98  public SqlParser(String sql, boolean isCallableStatement) {
99    parameterDefs = new ArrayList<ParameterDefinition>();
100    resultDefs = new ArrayList<ResultDefinition>();
101    openCurlyBrackets = new ArrayList<Integer>();
102    closeCurlyBrackets = new ArrayList<Integer>();
103    sqlLength = sql.length();
104 
105    if (sql.indexOf('{') >= 0) {
106      this.sql = parse(sql, isCallableStatement);
107    } else {
108      this.sql = sql;
109    }
110  }
111 
112  /**
113   * Returns the extracted parameter definitions.
114   * 
115   * @return extracted parameter definitions
116   */
117  public ParameterDefinition[] getParameterDefinitions() {
118    return parameterDefs.toArray(new ParameterDefinition[parameterDefs.size()]);
119  }
120 
121  /**
122   * Returns the extracted result definitions.
123   * 
124   * @return extracted result definitions
125   */
126  public ResultDefinition[] getResultDefinitions() {
127    return resultDefs.toArray(new ResultDefinition[resultDefs.size()]);
128  }
129 
130  /**
131   * Returns the parsed SQL statement without parameter and result definitions.
132   * 
133   * @return parsed SQL statement without parameter and result definitions
134   */
135  public String getSql() {
136    return sql;
137  }
138 
139  @SuppressWarnings("unchecked")
140  private String parse(String sql, boolean isCallableStatement) {
141    findCurlyBrackets(sql);
142    int curlyBracketIndex = 0;
143 
144    sql = sql.trim();
145    boolean callableInCurlyBrackets = false;
146    if (isCallableStatement && sql.charAt(0) == '{' && sql.charAt(sql.length() - 1) == '}') {
147      callableInCurlyBrackets = true;
148      sql = sql.substring(1, sql.length() - 1);
149      openCurlyBrackets.remove(0);
150      closeCurlyBrackets.remove(closeCurlyBrackets.size() - 1);
151    }
152    
153    List<String> saveStrings = new ArrayList<String>();
154    sql = extractStrings(sql, saveStrings);
155 
156    String[] tokenList = sql.split("\\s+");
157 
158    String preToken = "";
159    StringBuffer sbSql = new StringBuffer();
160    sqlIndex = 1;
161    for (String token : tokenList) {
162      if (token.length() == 0) {
163        sbSql.append(' ');
164        continue;
165      }
166      // increment sqlIndex for each '?'
167      for (int i = 0; i < token.length(); i++) {
168        if (token.charAt(i) == '?') {
169          sqlIndex++;
170        }
171      }
172      if (token.charAt(0) == '{') {
173        int indexComma = token.indexOf(',');
174        if (indexComma > 0) {
175          String leftToken = token.substring(0, indexComma) + "}";
176          String rightToken = "{" + token.substring(indexComma + 1);
177          if (isCallableStatement) {
178            if (leftToken.contains("%%") && !rightToken.contains("%%") || 
179                !leftToken.contains("%%") && rightToken.contains("%%")) {
180              parseDefinition(leftToken, preToken, sbSql, isCallableStatement, false, curlyBracketIndex);
181              parseDefinition(rightToken, preToken, sbSql, isCallableStatement, true, curlyBracketIndex);
182            } else {
183              int start = openCurlyBrackets.get(curlyBracketIndex);
184              int end = curlyBracketIndex < closeCurlyBrackets.size() ? closeCurlyBrackets.get(curlyBracketIndex) : sqlLength - 1;
185              throw new SqlParserException("Only one parameter and result definition are allowed: " + token, start, end - start + 1);
186            }
187          } else {
188            if (leftToken.contains("%%*")) {
189              parseDefinition(rightToken, preToken, sbSql, isCallableStatement, true, curlyBracketIndex);
190              parseDefinition(leftToken, preToken, sbSql, isCallableStatement, false, curlyBracketIndex);
191            } else if (rightToken.contains("%%*")) {
192              parseDefinition(leftToken, preToken, sbSql, isCallableStatement, true, curlyBracketIndex);
193              parseDefinition(rightToken, preToken, sbSql, isCallableStatement, false, curlyBracketIndex);
194            } else {
195              int start = openCurlyBrackets.get(curlyBracketIndex);
196              int end = curlyBracketIndex < closeCurlyBrackets.size() ? closeCurlyBrackets.get(curlyBracketIndex) : sqlLength - 1;
197              throw new SqlParserException("One of the definitions must be a map key definition: " + token, start, end - start + 1);
198            }
199          }
200          curlyBracketIndex++;
201        } else {
202          parseDefinition(token, preToken, sbSql, isCallableStatement, true, curlyBracketIndex);
203        }
204      } else {
205        sbSql.append(token).append(' ');
206      }
207      if (token.length() > 0 && !(token.charAt(0) == '{')) {
208        preToken = token;
209      }
210    }
211    sql = mergeStrings(sbSql.toString(), saveStrings);
212 
213    if (callableInCurlyBrackets) {
214      sql = "{ " + sql + " }";
215    }
216 
217    resultDefs = (List<ResultDefinition>)PartialDefinitionCombiner.combine(resultDefs);
218    parameterDefs = (List<ParameterDefinition>)PartialDefinitionCombiner.combine(parameterDefs);
219    
220    if (openCurlyBrackets.size() != closeCurlyBrackets.size()) {
221      throw new SqlParserException("Number of opening and closing curly brackets does not match", 0, sqlLength);
222    }
223    
224    return sql;
225  }
226 
227 
228  private void parseDefinition(String token, String preToken, StringBuffer sbSql, boolean isCallableStatement,
229      boolean insertQuestionMark, int curlyBracketIndex) {
230    int index = token.indexOf('%');
231    if (index + 1 < token.length() && token.charAt(index + 1) == '%') {
232      // result definition
233      if (!isCallableStatement) {
234        // extract the column name from the previous token
235        int wordIndex = preToken.length() - 1;
236        while (wordIndex >= 0) {
237          char c = preToken.charAt(wordIndex);
238          if (!Character.isLetterOrDigit(c) && c != '_') {
239            break;
240          }
241          wordIndex--;
242        }
243        String column = preToken.substring(wordIndex + 1);
244        resultDefs.add(parseResultDefinition(token, column, 0, curlyBracketIndex));
245      } else {
246        resultDefs.add(parseResultDefinition(token, null, sqlIndex, curlyBracketIndex));
247      }
248      if (insertQuestionMark && isCallableStatement) {
249        sqlIndex++;
250        sbSql.append("? ");
251      }
252    } else {
253      // parameter definition
254      parameterDefs.add(parseParameterDefinition(token, curlyBracketIndex));
255      if (insertQuestionMark) {
256        sqlIndex++;
257        sbSql.append("? ");
258      }
259    }
260  }
261 
262//  private static final Pattern RESULT_DEF_PATTERN = Pattern.compile("\\{([\\w-_]+)?%%((\\d+)|\\*|\\.(\\w+))?\\}");
263  private static final Pattern RESULT_DEF_PATTERN = 
264    Pattern.compile("\\{([\\w\\-]+)?%%((\\d+)|\\*|\\.(\\w+))?(@\\d+)?(\\[[\\w]+\\])?\\}");
265 
266  private ResultDefinition parseResultDefinition(String definition, String columnName, int sqlIndex, int curlyBracketIndex) {
267    String mappingType = null;
268    String field = null;
269    int constructorParameter = 0;
270    boolean isMapKey = false;
271    int partialDefinitionPart = 0;
272    String partialDefinitionGroup = null;
273 
274    Matcher matcher = RESULT_DEF_PATTERN.matcher(definition);
275    if (matcher.find() && matcher.group().equals(definition)) {
276      mappingType = matcher.group(1);
277      isMapKey = "*".equals(matcher.group(2));
278      if (matcher.group(3) != null) {
279            constructorParameter = Integer.valueOf(matcher.group(3));
280      }
281      field = matcher.group(4);
282      if (matcher.group(5) != null) {
283        partialDefinitionPart = Integer.valueOf(matcher.group(5).substring(1));
284      }
285      partialDefinitionGroup = matcher.group(6);
286 
287      ResultDefinitionImpl resultDef = new ResultDefinitionImpl();
288      resultDef.setType(mappingType);
289      if (columnName != null) {
290        resultDef.setColumns(new String[] { columnName });
291      } else {
292        resultDef.setIndexes(new int[] { sqlIndex });
293      }
294      resultDef.setField(field);
295      resultDef.setConstructorParameter(constructorParameter);
296      resultDef.setIsMapKey(isMapKey);
297      resultDef.setPartialDefinitionPart(partialDefinitionPart);
298      resultDef.setPartialDefinitionGroup(partialDefinitionGroup);
299      return resultDef;
300    } else {
301      int start = openCurlyBrackets.get(curlyBracketIndex);
302      int end = curlyBracketIndex < closeCurlyBrackets.size() ? closeCurlyBrackets.get(curlyBracketIndex) : sqlLength - 1;
303      throw new SqlParserException("Cannot parse definition: " + definition, start, end - start + 1);
304    }
305  }
306 
307//  private static final Pattern PARAMETER_DEF_PATTERN = Pattern.compile("\\{(\\w+)?%(\\d+)(\\.(\\w+))?\\}");
308  private static final Pattern PARAMETER_DEF_PATTERN = 
309    Pattern.compile("\\{([\\w\\-]+)?%(\\d+)(\\.(\\w+))?(@\\d+)?(\\[[\\w]+\\])?\\}");
310 
311  private ParameterDefinition parseParameterDefinition(String definition, int curlyBracketIndex) {
312    int parameterIndex = -1;
313    String mappingType = null;
314    String field = null;
315    int partialDefinitionPart = 0;
316    String partialDefinitionGroup = null;
317    
318    Matcher matcher = PARAMETER_DEF_PATTERN.matcher(definition);
319    if (matcher.find() && matcher.group().equals(definition)) {
320      mappingType = matcher.group(1);
321      parameterIndex = Integer.valueOf(matcher.group(2));
322      field = matcher.group(4);
323      if (matcher.group(5) != null) {
324        partialDefinitionPart = Integer.valueOf(matcher.group(5).substring(1));
325      }
326      partialDefinitionGroup = matcher.group(6);
327      
328      ParameterDefinitionImpl parameterDef = new ParameterDefinitionImpl();
329      parameterDef.setType(mappingType);
330      parameterDef.setParameter(parameterIndex);
331      parameterDef.setField(field);
332      parameterDef.setIndexes(new int[] { sqlIndex });
333      parameterDef.setPartialDefinitionPart(partialDefinitionPart);
334      parameterDef.setPartialDefinitionGroup(partialDefinitionGroup);
335      return parameterDef;
336    } else {
337      int start = openCurlyBrackets.get(curlyBracketIndex);
338      int end = curlyBracketIndex < closeCurlyBrackets.size() ? closeCurlyBrackets.get(curlyBracketIndex) : sqlLength - 1;
339      throw new SqlParserException("Cannot parse definition: " + definition, start, end - start + 1);
340    }
341  }
342 
343  private String mergeStrings(String sql, List<String> stringList) {
344    StringBuffer sb = new StringBuffer();
345    int index = 0;
346 
347    for (int i = 0; i < sql.length(); i++) {
348      char c = sql.charAt(i);
349      if (c == '#') {
350        sb.append(stringList.get(index++));
351      } else {
352        sb.append(c);
353      }
354    }
355 
356    return sb.toString();
357  }
358 
359  private String extractStrings(String sql, List<String> stringList) {
360    char quoteChar = '\0';
361    StringBuffer sb = new StringBuffer();
362    StringBuffer sbString = null;
363 
364    int i = 0;
365    while (i < sql.length()) {
366      char c = sql.charAt(i++);
367      if (quoteChar == '\0') {
368        if (c == '"' || c == '\'') {
369          // begin of string
370          quoteChar = c;
371          sbString = new StringBuffer();
372          sbString.append(c);
373        } else {
374          if (c == '{') {
375            quoteChar = c;
376            sb.append(' ');
377          }
378          sb.append(c);
379        }
380      } else {
381        if (quoteChar == '{') {
382          // remove white space from {...}
383          if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
384            sb.append(c);
385          }
386          if (c == '}') {
387            quoteChar = '\0';
388            sb.append(' ');
389          }
390        } else if (quoteChar == c) {
391          // end of string
392          quoteChar = '\0';
393          sbString.append(c);
394          stringList.add(sbString.toString());
395          sb.append('#');
396        } else {
397          sbString.append(c);
398        }
399      }
400    }
401 
402    return sb.toString();
403  }
404 
405  private void findCurlyBrackets(String sql) {
406    char quoteChar = '\0';
407 
408    int position = 0;
409    while (position < sql.length()) {
410      char c = sql.charAt(position);
411      if (quoteChar == '\0') {
412        if (c == '"' || c == '\'') {
413          // begin of string
414          quoteChar = c;
415        } else {
416          if (c == '{') {
417            openCurlyBrackets.add(position);
418          } else if (c == '}') {
419            closeCurlyBrackets.add(position);
420          }
421        }
422      } else {
423        if (quoteChar == c) {
424          // end of string
425          quoteChar = '\0';
426        } 
427      }
428      position++;
429    }
430  }
431}

[all classes][sf.qof.parser]
EMMA 2.0.5312 (C) Vladimir Roubtsov