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.test;
  
  import org.apache.calcite.avatica.AvaticaUtils;
  import org.apache.calcite.avatica.ConnectionConfigImpl;
  import org.apache.calcite.avatica.ConnectionProperty;
  import org.apache.calcite.avatica.util.ByteString;
  
  import org.junit.Test;
  
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.nio.charset.StandardCharsets;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.LinkedHashSet;
  import java.util.Locale;
  import java.util.Properties;
  import java.util.Set;
  
  import static org.apache.calcite.avatica.AvaticaUtils.skipFully;
  
  import static org.hamcrest.CoreMatchers.is;
  import static org.hamcrest.CoreMatchers.nullValue;
  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.junit.Assert.assertArrayEquals;
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertNotEquals;
  import static org.junit.Assert.fail;
  
  /**
   * Unit test for Avatica utilities.
   */
  public class AvaticaUtilsTest {
    /** Used by {@link #testInstantiatePlugin()}. */
    public static final ThreadLocal<String> STRING_THREAD_LOCAL =
        ThreadLocal.withInitial(() -> "not initialized");
  
    /** Also used by {@link #testInstantiatePlugin()}. */
    public static final ThreadLocal<Float> FLOAT_THREAD_LOCAL =
        ThreadLocal.withInitial(() -> Float.MIN_VALUE);
  
    /** Tests {@link AvaticaUtils#instantiatePlugin(Class, String)}. */
    @Test public void testInstantiatePlugin() {
      final String s =
          AvaticaUtils.instantiatePlugin(String.class, "java.lang.String");
      assertThat(s, is(""));
  
      final BigInteger b =
          AvaticaUtils.instantiatePlugin(BigInteger.class, "java.math.BigInteger#ONE");
      assertThat(b, is(BigInteger.ONE));
  
      // Class not found
      try {
        final BigInteger b2 =
            AvaticaUtils.instantiatePlugin(BigInteger.class, "org.apache.calcite.Abc");
        fail("expected error, got " + b2);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'org.apache.calcite.Abc' not valid as "
                + "'org.apache.calcite.Abc' not found in the classpath"));
      }
  
      // No instance of ABC
      try {
        final String s2 = AvaticaUtils.instantiatePlugin(String.class, "java.lang.String#ABC");
        fail("expected error, got " + s2);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'java.lang.String#ABC' not valid as "
                + "there is no 'ABC' field in the class of 'java.lang.String'"));
      }
  
      // The instance type is not the plugin type
      try {
        final String s2 = AvaticaUtils.instantiatePlugin(String.class, "java.math.BigInteger#ONE");
        fail("expected error, got " + s2);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'java.math.BigInteger#ONE' not valid as "
                + "cannot convert java.math.BigInteger to java.lang.String"));
      }
  
      // No default constructor or INSTANCE member
      try {
        final Integer i =
            AvaticaUtils.instantiatePlugin(Integer.class, "java.lang.Integer");
        fail("expected error, got " + i);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'java.lang.Integer' not valid as the default constructor is necessary,"
                + " but not found in the class of 'java.lang.Integer'"));
      }
  
      // Not valid for plugin type
      try {
        final BigInteger b2 =
            AvaticaUtils.instantiatePlugin(BigInteger.class, "java.lang.String");
        fail("expected error, got " + b2);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'java.lang.String' not valid for plugin type java.math.BigInteger"));
      }
  
      // Read from thread-local
      try {
        STRING_THREAD_LOCAL.set("abc");
        final String s2 =
            AvaticaUtils.instantiatePlugin(String.class,
                AvaticaUtilsTest.class.getName() + "#STRING_THREAD_LOCAL");
        assertThat(s2, is("abc"));
      } finally {
        STRING_THREAD_LOCAL.remove();
      }
  
      // Read from thread-local, wrong type
      try {
        STRING_THREAD_LOCAL.set("abc");
        final Integer i =
            AvaticaUtils.instantiatePlugin(Integer.class,
                AvaticaUtilsTest.class.getName() + "#STRING_THREAD_LOCAL");
        fail("expected error, got " + i);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'org.apache.calcite.avatica.test.AvaticaUtilsTest"
                + "#STRING_THREAD_LOCAL' not valid as cannot convert java.lang.String "
                + "to java.lang.Integer"));
      } finally {
        STRING_THREAD_LOCAL.remove();
      }
  
      // Read from thread-local, wrong type (array type, because it's tricky to
      // print correctly).
      try {
        STRING_THREAD_LOCAL.set("abc");
        final BigDecimal[] bigDecimals =
            AvaticaUtils.instantiatePlugin(BigDecimal[].class,
                AvaticaUtilsTest.class.getName() + "#STRING_THREAD_LOCAL");
        fail("expected error, got " + Arrays.toString(bigDecimals));
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'org.apache.calcite.avatica.test.AvaticaUtilsTest"
                + "#STRING_THREAD_LOCAL' not valid as cannot convert "
                + "java.lang.String to java.math.BigDecimal[]"));
      } finally {
        STRING_THREAD_LOCAL.remove();
      }
  
      // Read from thread-local, wrong type (private enum type, because it's
      // tricky to print correctly).
      try {
        STRING_THREAD_LOCAL.set("abc");
        final Weight weight =
            AvaticaUtils.instantiatePlugin(Weight.class,
                AvaticaUtilsTest.class.getName() + "#STRING_THREAD_LOCAL");
        fail("expected error, got " + weight);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'org.apache.calcite.avatica.test.AvaticaUtilsTest"
                + "#STRING_THREAD_LOCAL' not valid as cannot convert "
                + "java.lang.String to "
                + "org.apache.calcite.avatica.test.AvaticaUtilsTest.Weight"));
      } finally {
        STRING_THREAD_LOCAL.remove();
      }
  
      // Read from thread-local, wrong type (primitive type).
      try {
        STRING_THREAD_LOCAL.set("abc");
        final float f =
            AvaticaUtils.instantiatePlugin(float.class,
                AvaticaUtilsTest.class.getName() + "#STRING_THREAD_LOCAL");
        fail("expected error, got " + f);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'org.apache.calcite.avatica.test.AvaticaUtilsTest"
                + "#STRING_THREAD_LOCAL' not valid as cannot convert "
                + "java.lang.String to float"));
      } finally {
        STRING_THREAD_LOCAL.remove();
      }
  
      // Read from thread-local, primitive type.
      try {
        FLOAT_THREAD_LOCAL.set(2.5f);
        final float f =
            AvaticaUtils.instantiatePlugin(float.class,
                AvaticaUtilsTest.class.getName() + "#FLOAT_THREAD_LOCAL");
        fail("expected error, got " + f);
      } catch (Throwable e) {
        assertThat(e.getMessage(),
            is("Property 'org.apache.calcite.avatica.test.AvaticaUtilsTest"
                + "#FLOAT_THREAD_LOCAL' not valid as cannot convert "
                + "java.lang.Float to float"));
      } finally {
        FLOAT_THREAD_LOCAL.remove();
      }
    }
  
    /** Unit test for
     * {@link org.apache.calcite.avatica.AvaticaUtils#unique(java.lang.String)}. */
    @Test public void testUnique() {
      // Below, the "probably" comments indicate the strings that will be
      // generated the first time you run the test.
      final Set<String> list = new LinkedHashSet<>();
      list.add(AvaticaUtils.unique("a")); // probably "a"
      assertThat(list.size(), is(1));
      list.add(AvaticaUtils.unique("a")); // probably "a_1"
      assertThat(list.size(), is(2));
      list.add(AvaticaUtils.unique("b")); // probably "b"
      assertThat(list.size(), is(3));
      list.add(AvaticaUtils.unique("a_1")); // probably "a_1_3"
      assertThat(list.size(), is(4));
      list.add(AvaticaUtils.unique("A")); // probably "A"
      assertThat(list.size(), is(5));
      list.add(AvaticaUtils.unique("a")); // probably "a_5"
      assertThat(list.size(), is(6));
    }
  
    /** Tests connect string properties. */
    @Test public void testConnectionProperty() {
      final ConnectionPropertyImpl n = new ConnectionPropertyImpl("N",
          BigDecimal.valueOf(100), ConnectionProperty.Type.NUMBER);
  
      final Properties properties = new Properties();
      ConnectionConfigImpl.PropEnv env = n.wrap(properties);
      assertThat(env.getInt(), is(100));
      assertThat(env.getInt(-45), is(-45));
      properties.setProperty(n.name, "123");
      assertThat(env.getInt(), is(100));
      env = n.wrap(properties);
      assertThat(env.getInt(), is(123));
      assertThat(env.getInt(-45), is(123));
  
      properties.setProperty(n.name, "10k");
      env = n.wrap(properties);
      assertThat(env.getInt(), is(10 * 1024));
  
      properties.setProperty(n.name, "-0.5k");
      env = n.wrap(properties);
      assertThat(env.getInt(), is(-512));
  
      properties.setProperty(n.name, "10m");
      env = n.wrap(properties);
      assertThat(env.getInt(), is(10 * 1024 * 1024));
      assertThat(env.getLong(), is(10L * 1024 * 1024));
      assertThat(env.getDouble(), is(10D * 1024 * 1024));
  
      properties.setProperty(n.name, "-2M");
      env = n.wrap(properties);
      assertThat(env.getInt(), is(-2 * 1024 * 1024));
  
      properties.setProperty(n.name, "10g");
      env = n.wrap(properties);
      assertThat(env.getLong(), is(10L * 1024 * 1024 * 1024));
  
      final ConnectionPropertyImpl b = new ConnectionPropertyImpl("B",
          true, ConnectionProperty.Type.BOOLEAN);
  
      env = b.wrap(properties);
      assertThat(env.getBoolean(), is(true));
      assertThat(env.getBoolean(true), is(true));
      assertThat(env.getBoolean(false), is(false));
  
      properties.setProperty(b.name, "false");
      env = b.wrap(properties);
      assertThat(env.getBoolean(), is(false));
  
      final ConnectionPropertyImpl s = new ConnectionPropertyImpl("S",
          "foo", ConnectionProperty.Type.STRING);
  
      env = s.wrap(properties);
      assertThat(env.getString(), is("foo"));
      assertThat(env.getString("baz"), is("baz"));
  
      properties.setProperty(s.name, "  ");
      env = s.wrap(properties);
      assertThat(env.getString(), is("  "));
  
      try {
        final ConnectionPropertyImpl t =
            new ConnectionPropertyImpl("T", null, ConnectionProperty.Type.ENUM);
        fail("should throw if you specify an enum property without a class, got "
            + t);
      } catch (AssertionError e) {
        assertThat(e.getMessage(), is("must specify value class for an ENUM"));
        // ok
      }
  
      // An enum with a default value
      final ConnectionPropertyImpl t = new ConnectionPropertyImpl("T", Size.BIG,
          ConnectionProperty.Type.ENUM, Size.class);
      env = t.wrap(properties);
      assertThat(env.getEnum(Size.class), is(Size.BIG));
      assertThat(env.getEnum(Size.class, Size.SMALL), is(Size.SMALL));
      assertThat(env.getEnum(Size.class, null), nullValue());
      try {
        final Weight envEnum = env.getEnum(Weight.class, null);
        fail("expected error, got " + envEnum);
      } catch (AssertionError e) {
        assertThat(e.getMessage(),
            is("wrong value class; expected " + Size.class));
      }
  
      // An enum with a default value that is an anonymous enum,
      // not specifying value type.
      final ConnectionPropertyImpl v = new ConnectionPropertyImpl("V",
          Size.SMALL, ConnectionProperty.Type.ENUM);
      env = v.wrap(properties);
      assertThat(env.getEnum(Size.class), is(Size.SMALL));
      assertThat(env.getEnum(Size.class, Size.BIG), is(Size.BIG));
      assertThat(env.getEnum(Size.class, null), nullValue());
      try {
        final Weight envEnum = env.getEnum(Weight.class, null);
        fail("expected error, got " + envEnum);
      } catch (AssertionError e) {
        assertThat(e.getMessage(),
            is("wrong value class; expected " + Size.class));
      }
  
      // An enum with no default value
      final ConnectionPropertyImpl u = new ConnectionPropertyImpl("U", null,
          ConnectionProperty.Type.ENUM, Size.class);
      env = u.wrap(properties);
      assertThat(env.getEnum(Size.class), nullValue());
      assertThat(env.getEnum(Size.class, Size.SMALL), is(Size.SMALL));
      assertThat(env.getEnum(Size.class, null), nullValue());
      try {
        final Weight envEnum = env.getEnum(Weight.class, null);
        fail("expected error, got " + envEnum);
      } catch (AssertionError e) {
        assertThat(e.getMessage(),
            is("wrong value class; expected " + Size.class));
      }
    }
  
    @Test public void testLongToIntegerTranslation() {
      long[] longValues = new long[] {Integer.MIN_VALUE, -5, 0, 1, Integer.MAX_VALUE,
          ((long) Integer.MAX_VALUE) + 1L, Long.MAX_VALUE};
      int[] convertedValues = AvaticaUtils.toSaturatedInts(longValues);
      int[] intValues = new int[] {Integer.MIN_VALUE, -5, 0, 1, Integer.MAX_VALUE,
          Integer.MAX_VALUE, Integer.MAX_VALUE};
      assertArrayEquals(convertedValues, intValues);
    }
  
    @Test public void testByteString() {
      final byte[] bytes = {3, 14, 15, 92, 0, 65, 35, 0};
      final ByteString s = new ByteString(bytes);
      final ByteString s2 = new ByteString(bytes.clone());
      final ByteString s3 = new ByteString(new byte[0]);
      final ByteString s4 = new ByteString(new byte[] {0});
      final ByteString s5 = new ByteString(new byte[]{15, 92});
  
      // length
      assertThat(s.length(), is(8));
      assertThat(s3.length(), is(0));
      assertThat(s4.length(), is(1));
  
      // equals and hashCode
      assertThat(s.hashCode(), is(s2.hashCode()));
      assertThat(s.equals(s2), is(true));
      assertThat(s2.equals(s), is(true));
      assertThat(s.equals(s3), is(false));
      assertThat(s3.equals(s), is(false));
  
      // toString
      assertThat(s.toString(), is("030e0f5c00412300"));
      assertThat(s3.toString(), is(""));
      assertThat(s4.toString(), is("00"));
  
      // indexOf
      assertThat(s.indexOf(s3), is(0));
      assertThat(s.indexOf(s3, 5), is(5));
      assertThat(s.indexOf(s3, 15), is(-1));
      assertThat(s.indexOf(s4), is(4));
      assertThat(s.indexOf(s4, 4), is(4));
      assertThat(s.indexOf(s4, 5), is(7));
      assertThat(s.indexOf(s5), is(2));
      assertThat(s.indexOf(s5, 2), is(2));
      assertThat(s.indexOf(s5, 3), is(-1));
      assertThat(s.indexOf(s5, 7), is(-1));
  
      // substring
      assertThat(s.substring(8), is(s3));
      assertThat(s.substring(7), is(s4));
      assertThat(s.substring(0), is(s));
  
      // getBytes
      assertThat(s.getBytes().length, is(8));
      assertThat(Arrays.equals(s.getBytes(), bytes), is(true));
      assertThat(s.getBytes()[3], is((byte) 92));
      final byte[] copyBytes = s.getBytes();
      copyBytes[3] = 11;
      assertThat(s.getBytes()[3], is((byte) 92));
      assertThat(s, is(s2));
  
      // startsWith
      assertThat(s.startsWith(s), is(true));
      assertThat(s.startsWith(s.substring(0, 1)), is(true));
      assertThat(s.startsWith(s.substring(0, 3)), is(true));
      assertThat(s.startsWith(s3), is(true)); // ""
      assertThat(s.startsWith(s4), is(false)); // "\0"
      assertThat(s3.startsWith(s3), is(true)); // "" starts with ""
  
      // startsWith offset 0
      assertThat(s.startsWith(s, 0), is(true));
      assertThat(s.startsWith(s.substring(0, 1), 0), is(true));
      assertThat(s.startsWith(s.substring(0, 3), 0), is(true));
      assertThat(s.startsWith(s3, 0), is(true)); // ""
      assertThat(s.startsWith(s4, 0), is(false)); // "\0"
      assertThat(s3.startsWith(s3, 0), is(true)); // "" starts with ""
  
      // startsWith, other offsets
      assertThat(s.startsWith(s, 1), is(false));
      assertThat(s.startsWith(s3, 1), is(true));
      assertThat(s.startsWith(s3, s.length() - 1), is(true));
      assertThat(s.startsWith(s3, s.length()), is(true));
      assertThat(s.startsWith(s3, s.length() + 1), is(false));
  
      // endsWith
      assertThat(reverse(s).endsWith(reverse(s)), is(true));
      assertThat(reverse(s).endsWith(reverse(s.substring(0, 1))), is(true));
      assertThat(reverse(s).endsWith(reverse(s.substring(0, 3))), is(true));
      assertThat(reverse(s).endsWith(reverse(s3)), is(true)); // ""
      assertThat(reverse(s).endsWith(reverse(s4)), is(false)); // "\0"
      assertThat(reverse(s3).endsWith(reverse(s3)), is(true));
    }
  
    private ByteString reverse(ByteString s) {
      final byte[] bytes = s.getBytes();
      for (int i = 0, j = bytes.length - 1; i < j; i++, j--) {
        byte b = bytes[i];
        bytes[i] = bytes[j];
        bytes[j] = b;
      }
      return new ByteString(bytes);
    }
  
    @Test public void testSkipFully() throws IOException {
      InputStream in = of("");
      assertEquals(0, in.available());
      skipFully(in);
      assertEquals(0, in.available());
  
      in = of("asdf");
      assertEquals(4, in.available());
      skipFully(in);
      assertEquals(0, in.available());
  
      in = of("asdfasdf");
      for (int i = 0; i < 4; i++) {
        assertNotEquals(-1, in.read());
      }
      assertEquals(4, in.available());
      skipFully(in);
      assertEquals(0, in.available());
    }
  
    /** Returns an InputStream of UTF-8 encoded bytes from the provided string */
    InputStream of(String str) {
      return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
    }
  
    /** Dummy implementation of {@link ConnectionProperty}. */
    private static class ConnectionPropertyImpl implements ConnectionProperty {
      private final String name;
      private final Object defaultValue;
      private final Class<?> valueClass;
      private Type type;
  
      ConnectionPropertyImpl(String name, Object defaultValue, Type type) {
        this(name, defaultValue, type, null);
      }
  
      ConnectionPropertyImpl(String name, Object defaultValue, Type type,
          Class valueClass) {
        this.name = name;
        this.defaultValue = defaultValue;
        this.type = type;
        this.valueClass = type.deduceValueClass(defaultValue, valueClass);
        if (!type.valid(defaultValue, this.valueClass)) {
          throw new AssertionError(name);
        }
      }
  
      public String name() {
        return name.toUpperCase(Locale.ROOT);
      }
  
      public String camelName() {
        return name.toLowerCase(Locale.ROOT);
      }
  
      public Object defaultValue() {
        return defaultValue;
      }
  
      public Type type() {
        return type;
      }
  
      public Class valueClass() {
        return valueClass;
      }
  
      public ConnectionConfigImpl.PropEnv wrap(Properties properties) {
        final HashMap<String, ConnectionProperty> map = new HashMap<>();
        map.put(name, this);
        return new ConnectionConfigImpl.PropEnv(
            ConnectionConfigImpl.parse(properties, map), this);
      }
  
      public boolean required() {
        return false;
      }
    }
  
    /** How large? */
    private enum Size {
      BIG,
      SMALL {
      }
    }
  
    /** How heavy? */
    private enum Weight {
      HEAVY, LIGHT
    }
  }
  
  // End AvaticaUtilsTest.java
  