ConnectStringParser.java

/*
 * 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