PageRenderTime 3ms CodeModel.GetById 72ms app.highlight 46ms RepoModel.GetById 14ms app.codeStats 1ms

/avatica/src/main/java/net/hydromatic/avatica/ConnectStringParser.java

https://github.com/ahoy-jon/optiq
Java | 387 lines | 244 code | 19 blank | 124 comment | 40 complexity | 6a767e69db448f3a979b716a073df191 MD5 | raw file
  1/*
  2// Licensed to Julian Hyde under one or more contributor license
  3// agreements. See the NOTICE file distributed with this work for
  4// additional information regarding copyright ownership.
  5//
  6// Julian Hyde licenses this file to you under the Apache License,
  7// Version 2.0 (the "License"); you may not use this file except in
  8// compliance with the License. You may obtain a copy of the License at:
  9//
 10// http://www.apache.org/licenses/LICENSE-2.0
 11//
 12// Unless required by applicable law or agreed to in writing, software
 13// distributed under the License is distributed on an "AS IS" BASIS,
 14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15// See the License for the specific language governing permissions and
 16// limitations under the License.
 17*/
 18package net.hydromatic.avatica;
 19
 20import java.sql.SQLException;
 21import java.util.Map;
 22import java.util.Properties;
 23
 24/**
 25 * ConnectStringParser is a utility class that parses or creates a JDBC connect
 26 * string according to the OLE DB connect string syntax described at <a
 27 * href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/oledb/htm/oledbconnectionstringsyntax.asp">
 28 * OLE DB Connection String Syntax</a>.
 29 *
 30 * <p>This code was adapted from Mondrian's mondrian.olap.Util class.
 31 * The primary differences between this and its Mondrian progenitor are:
 32 *
 33 * <ul>
 34 * <li>use of regular {@link Properties} for compatibility with the JDBC API
 35 * (replaces Mondrian's use of its own order-preserving and case-insensitive
 36 * PropertyList, found in Util.java at link above)</li>
 37 * <li>ability to pass to {@link #parse} a pre-existing Properties object into
 38 * which properties are to be parsed, possibly overriding prior values</li>
 39 * <li>use of {@link SQLException}s rather than unchecked {@link
 40 * RuntimeException}s</li>
 41 * <li>static members for parsing and creating connect strings</li>
 42 * </ul>
 43 *
 44 * <p>ConnectStringParser has a private constructor. Callers use the static
 45 * members:
 46 *
 47 * <dl>
 48 * <dt>{@link #parse(String)}
 49 * <dd>Parses the connect string into a new Properties object.
 50 *
 51 * <dt>{@link #parse(String, Properties)}
 52 * <dd>Parses the connect string into an existing Properties object.
 53 *
 54 * <dt>{@link #getParamString(Properties)}
 55 * <dd>Returns a param string, quoted and escaped as needed, to represent the
 56 * supplied name-value pairs.
 57 * </dl>
 58 */
 59public class ConnectStringParser {
 60  //~ Instance fields --------------------------------------------------------
 61
 62  private final String s;
 63  private final int n;
 64  private int i;
 65  private final StringBuilder nameBuf = new StringBuilder();
 66  private final StringBuilder valueBuf = new StringBuilder();
 67
 68  //~ Constructors -----------------------------------------------------------
 69
 70  /**
 71   * Creates a new connect string parser.
 72   *
 73   * @param s connect string to parse
 74   *
 75   * @see #parse(String)
 76   * @see #parse(String, Properties)
 77   */
 78  private ConnectStringParser(String s) {
 79    this.s = s;
 80    this.i = 0;
 81    this.n = s.length();
 82  }
 83
 84  //~ Methods ----------------------------------------------------------------
 85
 86  /**
 87   * Parses the connect string into a new Properties object.
 88   *
 89   * @param s connect string to parse
 90   *
 91   * @return properties object with parsed params
 92   *
 93   * @throws SQLException error parsing name-value pairs
 94   */
 95  public static Properties parse(String s)
 96    throws SQLException {
 97    return new ConnectStringParser(s).parseInternal(null);
 98  }
 99
100  /**
101   * Parses the connect string into an existing Properties object.
102   *
103   * @param s connect string to parse
104   * @param props optional properties object, may be <code>null</code>
105   *
106   * @return properties object with parsed params; if an input <code>
107   * props</code> was supplied, any duplicate properties will have been
108   * replaced by those from the connect string.
109   *
110   * @throws SQLException error parsing name-value pairs
111   */
112  public static Properties parse(String s, Properties props)
113    throws SQLException {
114    return new ConnectStringParser(s).parseInternal(props);
115  }
116
117  /**
118   * Parses the connect string into a Properties object. Note that the string
119   * can only be parsed once. Subsequent calls return empty/unchanged
120   * Properties.
121   *
122   * @param props optional properties object, may be <code>null</code>
123   *
124   * @return properties object with parsed params; if an input <code>
125   * props</code> was supplied, any duplicate properties will have been
126   * replaced by those from the connect string.
127   *
128   * @throws SQLException error parsing name-value pairs
129   */
130  Properties parseInternal(Properties props)
131    throws SQLException {
132    if (props == null) {
133      props = new Properties();
134    }
135    while (i < n) {
136      parsePair(props);
137    }
138    return props;
139  }
140
141  /**
142   * Reads "name=value;" or "name=value<EOF>".
143   *
144   * @throws SQLException error parsing value
145   */
146  void parsePair(Properties props)
147    throws SQLException {
148    String name = parseName();
149    String value;
150    if (i >= n) {
151      value = "";
152    } else if (s.charAt(i) == ';') {
153      i++;
154      value = "";
155    } else {
156      value = parseValue();
157    }
158    props.put(name, value);
159  }
160
161  /**
162   * Reads "name=". Name can contain equals sign if equals sign is doubled.
163   */
164  String parseName() {
165    nameBuf.setLength(0);
166    while (true) {
167      char c = s.charAt(i);
168      switch (c) {
169      case '=':
170        i++;
171        if ((i < n) && ((c = s.charAt(i)) == '=')) {
172          // doubled equals sign; take one of them, and carry on
173          i++;
174          nameBuf.append(c);
175          break;
176        }
177        String name = nameBuf.toString();
178        name = name.trim();
179        return name;
180      case ' ':
181        if (nameBuf.length() == 0) {
182          // ignore preceding spaces
183          i++;
184          break;
185        }
186        // fall through
187      default:
188        nameBuf.append(c);
189        i++;
190        if (i >= n) {
191          return nameBuf.toString().trim();
192        }
193      }
194    }
195  }
196
197  /**
198   * Reads "value;" or "value<EOF>"
199   *
200   * @throws SQLException if find an unterminated quoted value
201   */
202  String parseValue()
203    throws SQLException {
204    char c;
205
206    // skip over leading white space
207    while ((c = s.charAt(i)) == ' ') {
208      i++;
209      if (i >= n) {
210        return "";
211      }
212    }
213    if ((c == '"') || (c == '\'')) {
214      String value = parseQuoted(c);
215
216      // skip over trailing white space
217      while ((i < n) && ((c = s.charAt(i)) == ' ')) {
218        i++;
219      }
220      if (i >= n) {
221        return value;
222      } else if (s.charAt(i) == ';') {
223        i++;
224        return value;
225      } else {
226        throw new SQLException(
227            "quoted value ended too soon, at position " + i
228            + " in '" + s + "'");
229      }
230    } else {
231      String value;
232      int semi = s.indexOf(';', i);
233      if (semi >= 0) {
234        value = s.substring(i, semi);
235        i = semi + 1;
236      } else {
237        value = s.substring(i);
238        i = n;
239      }
240      return value.trim();
241    }
242  }
243
244  /**
245   * Reads a string quoted by a given character. Occurrences of the quoting
246   * character must be doubled. For example, <code>parseQuoted('"')</code>
247   * reads <code>"a ""new"" string"</code> and returns <code>a "new"
248   * string</code>.
249   *
250   * @throws SQLException if find an unterminated quoted value
251   */
252  String parseQuoted(char q)
253    throws SQLException {
254    char c = s.charAt(i++);
255    if (c != q) {
256      throw new AssertionError("c != q: c=" + c + " q=" + q);
257    }
258    valueBuf.setLength(0);
259    while (i < n) {
260      c = s.charAt(i);
261      if (c == q) {
262        i++;
263        if (i < n) {
264          c = s.charAt(i);
265          if (c == q) {
266            valueBuf.append(c);
267            i++;
268            continue;
269          }
270        }
271        return valueBuf.toString();
272      } else {
273        valueBuf.append(c);
274        i++;
275      }
276    }
277    throw new SQLException(
278        "Connect string '" + s
279        + "' contains unterminated quoted value '"
280        + valueBuf.toString() + "'");
281  }
282
283  /**
284   * Returns a param string, quoted and escaped as needed, to represent the
285   * supplied name-value pairs.
286   *
287   * @param props name-value pairs
288   *
289   * @return param string, never <code>null</code>
290   */
291  public static String getParamString(Properties props) {
292    if (props == null) {
293      return "";
294    }
295
296    StringBuilder buf = new StringBuilder();
297    for (Map.Entry<String, String> entry : toMap(props).entrySet()) {
298      final String name = entry.getKey();
299      final String value = entry.getValue();
300      String quote = "";
301      if (buf.length() > 0) {
302        buf.append(';');
303      }
304
305      // write parameter name
306      if (name.startsWith(" ") || name.endsWith(" ")) {
307        quote = "'";
308        buf.append(quote);
309      }
310      int len = name.length();
311      for (int i = 0; i < len; ++i) {
312        char c = name.charAt(i);
313        if (c == '=') {
314          buf.append('=');
315        }
316        buf.append(c);
317      }
318
319      buf.append(quote); // might be empty
320      quote = "";
321
322      buf.append('=');
323
324      // write parameter value
325      len = value.length();
326      boolean hasSemi = value.indexOf(';') >= 0;
327      boolean hasSQ = value.indexOf("'") >= 0;
328      boolean hasDQ = value.indexOf('"') >= 0;
329      if (value.startsWith(" ") || value.endsWith(" ")) {
330        quote = "'";
331      } else if (hasSemi || hasSQ || hasDQ) {
332        // try to choose the least painful quote
333        if (value.startsWith("\"")) {
334          quote = "'";
335        } else if (value.startsWith("'")) {
336          quote = "\"";
337        } else {
338          quote = hasSQ ? "\"" : "'";
339        }
340      }
341      char q;
342      if (quote.length() > 0) {
343        buf.append(quote);
344        q = quote.charAt(0);
345      } else {
346        q = '\0';
347      }
348      for (int i = 0; i < len; ++i) {
349        char c = value.charAt(i);
350        if (c == q) {
351          buf.append(q);
352        }
353        buf.append(c);
354      }
355      buf.append(quote); // might be empty
356    }
357
358    return buf.toString();
359  }
360
361  /**
362   * Converts a {@link Properties} object to a <code>{@link Map}&lt;String,
363   * String&gt;</code>.
364   *
365   * <p>This is necessary because {@link Properties} is a dinosaur class. It
366   * ought to extend <code>Map&lt;String,String&gt;</code>, but instead
367   * extends <code>{@link java.util.Hashtable}&lt;Object,Object&gt;</code>.
368   *
369   * <p>Typical usage, to iterate over a {@link Properties}:
370   *
371   * <blockquote>
372   * <code>
373   * Properties properties;<br>
374   * for (Map.Entry&lt;String, String&gt; entry =
375   * Util.toMap(properties).entrySet()) {<br>
376   *   println("key=" + entry.getKey() + ", value=" + entry.getValue());<br>
377   * }
378   * </code>
379   * </blockquote>
380   */
381  public static Map<String, String> toMap(
382      final Properties properties) {
383    return (Map) properties;
384  }
385}
386
387// End ConnectStringParser.java