blob: 9efbcf1132a2cc13a4770c6aecf9f4fe4d05d030 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.avatica;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
/**
* ConnectStringParser is a utility class that parses or creates a JDBC connect
* string according to the
* <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms722656(v=vs.85).aspx">
* OLE DB Connection String Syntax</a>.
*
* <p>This code was adapted from Mondrian's mondrian.olap.Util class.
* The primary differences between this and its Mondrian progenitor are:
*
* <ul>
* <li>use of regular {@link Properties} for compatibility with the JDBC API
* (replaces Mondrian's use of its own order-preserving and case-insensitive
* PropertyList)</li>
*
* <li>ability to pass to {@link #parse} a pre-existing Properties object into
* which properties are to be parsed, possibly overriding prior values</li>
*
* <li>use of {@link SQLException}s rather than unchecked
* {@link RuntimeException}s</li>
*
* <li>static members for parsing and creating connect strings</li>
*
* </ul>
*
* <p>ConnectStringParser has a private constructor. Callers use the static
* members:
*
* <dl>
* <dt>{@link #parse(String)}
* <dd>Parses the connect string into a new Properties object.
*
* <dt>{@link #parse(String, Properties)}
* <dd>Parses the connect string into an existing Properties object.
*
* <dt>{@link #getParamString(Properties)}
* <dd>Returns a param string, quoted and escaped as needed, to represent the
* supplied name-value pairs.
* </dl>
*/
public class ConnectStringParser {
//~ Instance fields --------------------------------------------------------
private final String s;
private final int n;
private int i;
private final StringBuilder nameBuf = new StringBuilder();
private final StringBuilder valueBuf = new StringBuilder();
//~ Constructors -----------------------------------------------------------
/**
* Creates a new connect string parser.
*
* @param s connect string to parse
*
* @see #parse(String)
* @see #parse(String, Properties)
*/
private ConnectStringParser(String s) {
this.s = s;
this.i = 0;
this.n = s.length();
}
//~ Methods ----------------------------------------------------------------
/**
* Parses the connect string into a new Properties object.
*
* @param s connect string to parse
*
* @return properties object with parsed params
*
* @throws SQLException error parsing name-value pairs
*/
public static Properties parse(String s)
throws SQLException {
return new ConnectStringParser(s).parseInternal(null);
}
/**
* Parses the connect string into an existing Properties object.
*
* @param s connect string to parse
* @param props optional properties object, may be <code>null</code>
*
* @return properties object with parsed params; if an input <code>
* props</code> was supplied, any duplicate properties will have been
* replaced by those from the connect string.
*
* @throws SQLException error parsing name-value pairs
*/
public static Properties parse(String s, Properties props)
throws SQLException {
return new ConnectStringParser(s).parseInternal(props);
}
/**
* Parses the connect string into a Properties object. Note that the string
* can only be parsed once. Subsequent calls return empty/unchanged
* Properties. The original <code>props</code> argument is not altered.
*
* @param props optional properties object, may be <code>null</code>
*
* @return properties object with parsed params; if an input <code>
* props</code> was supplied, any duplicate properties will have been
* replaced by those from the connect string.
*
* @throws SQLException error parsing name-value pairs
*/
Properties parseInternal(final Properties props)
throws SQLException {
final Properties newProps;
if (props == null) {
newProps = new Properties();
} else {
newProps = (Properties) props.clone();
}
while (i < n) {
parsePair(newProps);
}
return newProps;
}
/**
* Reads "name=value;" or "name=value&lt;EOF&gt;".
*
* @throws SQLException error parsing value
*/
void parsePair(Properties props)
throws SQLException {
String name = parseName();
String value;
if (i >= n) {
value = "";
} else if (s.charAt(i) == ';') {
i++;
value = "";
} else {
value = parseValue();
}
props.put(name, value);
}
/**
* Reads "name=". Name can contain equals sign if equals sign is doubled.
*/
String parseName() {
nameBuf.setLength(0);
while (true) {
char c = s.charAt(i);
switch (c) {
case '=':
i++;
if ((i < n) && ((c = s.charAt(i)) == '=')) {
// doubled equals sign; take one of them, and carry on
i++;
nameBuf.append(c);
break;
}
String name = nameBuf.toString();
name = name.trim();
return name;
case ' ':
if (nameBuf.length() == 0) {
// ignore preceding spaces
i++;
break;
}
// fall through
default:
nameBuf.append(c);
i++;
if (i >= n) {
return nameBuf.toString().trim();
}
}
}
}
/**
* Reads "value;" or "value&lt;EOF&gt;"
*
* @throws SQLException if find an unterminated quoted value
*/
String parseValue()
throws SQLException {
char c;
// skip over leading white space
while ((c = s.charAt(i)) == ' ') {
i++;
if (i >= n) {
return "";
}
}
if ((c == '"') || (c == '\'')) {
String value = parseQuoted(c);
// skip over trailing white space
while (i < n && s.charAt(i) == ' ') {
i++;
}
if (i >= n) {
return value;
} else if (s.charAt(i) == ';') {
i++;
return value;
} else {
throw new SQLException(
"quoted value ended too soon, at position " + i
+ " in '" + s + "'");
}
} else {
String value;
int semi = s.indexOf(';', i);
if (semi >= 0) {
value = s.substring(i, semi);
i = semi + 1;
} else {
value = s.substring(i);
i = n;
}
return value.trim();
}
}
/**
* Reads a string quoted by a given character. Occurrences of the quoting
* character must be doubled. For example, <code>parseQuoted('"')</code>
* reads <code>"a ""new"" string"</code> and returns <code>a "new"
* string</code>.
*
* @throws SQLException if find an unterminated quoted value
*/
String parseQuoted(char q)
throws SQLException {
char c = s.charAt(i++);
if (c != q) {
throw new AssertionError("c != q: c=" + c + " q=" + q);
}
valueBuf.setLength(0);
while (i < n) {
c = s.charAt(i);
if (c == q) {
i++;
if (i < n) {
c = s.charAt(i);
if (c == q) {
valueBuf.append(c);
i++;
continue;
}
}
return valueBuf.toString();
} else {
valueBuf.append(c);
i++;
}
}
throw new SQLException(
"Connect string '" + s
+ "' contains unterminated quoted value '"
+ valueBuf.toString() + "'");
}
/**
* Returns a param string, quoted and escaped as needed, to represent the
* supplied name-value pairs.
*
* @param props name-value pairs
*
* @return param string, never <code>null</code>
*/
public static String getParamString(Properties props) {
if (props == null) {
return "";
}
StringBuilder buf = new StringBuilder();
for (Map.Entry<String, String> entry : toMap(props).entrySet()) {
final String name = entry.getKey();
final String value = entry.getValue();
String quote = "";
if (buf.length() > 0) {
buf.append(';');
}
// write parameter name
if (name.startsWith(" ") || name.endsWith(" ")) {
quote = "'";
buf.append(quote);
}
int len = name.length();
for (int i = 0; i < len; ++i) {
char c = name.charAt(i);
if (c == '=') {
buf.append('=');
}
buf.append(c);
}
buf.append(quote); // might be empty
quote = "";
buf.append('=');
// write parameter value
len = value.length();
boolean hasSemi = value.indexOf(';') >= 0;
boolean hasSQ = value.indexOf('\'') >= 0;
boolean hasDQ = value.indexOf('"') >= 0;
if (value.startsWith(" ") || value.endsWith(" ")) {
quote = "'";
} else if (hasSemi || hasSQ || hasDQ) {
// try to choose the least painful quote
if (value.startsWith("\"")) {
quote = "'";
} else if (value.startsWith("'")) {
quote = "\"";
} else {
quote = hasSQ ? "\"" : "'";
}
}
char q;
if (quote.length() > 0) {
buf.append(quote);
q = quote.charAt(0);
} else {
q = '\0';
}
for (int i = 0; i < len; ++i) {
char c = value.charAt(i);
if (c == q) {
buf.append(q);
}
buf.append(c);
}
buf.append(quote); // might be empty
}
return buf.toString();
}
/**
* Converts a {@link Properties} object to a <code>{@link Map}&lt;String,
* String&gt;</code>.
*
* <p>This is necessary because {@link Properties} is a dinosaur class. It
* ought to extend <code>Map&lt;String,String&gt;</code>, but instead
* extends <code>{@link java.util.Hashtable}&lt;Object,Object&gt;</code>.
*
* <p>Typical usage, to iterate over a {@link Properties}:
*
* <blockquote>
* <code>
* Properties properties;<br>
* for (Map.Entry&lt;String, String&gt; entry =
* Util.toMap(properties).entrySet()) {<br>
* println("key=" + entry.getKey() + ", value=" + entry.getValue());<br>
* }
* </code>
* </blockquote>
*/
public static Map<String, String> toMap(
final Properties properties) {
//noinspection unchecked
return (Map) properties;
}
}
// End ConnectStringParser.java