PageRenderTime 62ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 0ms

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