AvaticaUtilsTest.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.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