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