All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.spanner.connection.ClientSideStatementValueConverters Maven / Gradle / Ivy

There is a newer version: 6.81.1
Show newest version
/*
 * Copyright 2019 Google LLC
 *
 * Licensed 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 com.google.cloud.spanner.connection;

import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.parseTimeUnit;
import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.toChronoUnit;

import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.TimestampBound.Mode;
import com.google.cloud.spanner.connection.PgTransactionMode.AccessMode;
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.spanner.v1.DirectedReadOptions;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Contains all {@link ClientSideStatementValueConverter} implementations. */
class ClientSideStatementValueConverters {
  /** Map for mapping case-insensitive strings to enums. */
  private static final class CaseInsensitiveEnumMap> {
    private final Map map = new HashMap<>();

    /** Create an map using the name of the enum elements as keys. */
    private CaseInsensitiveEnumMap(Class elementType) {
      this(elementType, Enum::name);
    }

    /** Create a map using the specific function to get the key per enum value. */
    private CaseInsensitiveEnumMap(Class elementType, Function keyFunction) {
      Preconditions.checkNotNull(elementType);
      Preconditions.checkNotNull(keyFunction);
      EnumSet set = EnumSet.allOf(elementType);
      for (E e : set) {
        if (map.put(keyFunction.apply(e).toUpperCase(), e) != null) {
          throw new IllegalArgumentException(
              "Enum contains multiple elements with the same case-insensitive key");
        }
      }
    }

    private E get(String value) {
      Preconditions.checkNotNull(value);
      return map.get(value.toUpperCase());
    }
  }

  /** Converter from string to {@link Boolean} */
  static class BooleanConverter implements ClientSideStatementValueConverter {

    public BooleanConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return Boolean.class;
    }

    @Override
    public Boolean convert(String value) {
      if ("true".equalsIgnoreCase(value)) {
        return Boolean.TRUE;
      }
      if ("false".equalsIgnoreCase(value)) {
        return Boolean.FALSE;
      }
      return null;
    }
  }

  /** Converter from string to {@link Boolean} */
  static class PgBooleanConverter implements ClientSideStatementValueConverter {

    public PgBooleanConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return Boolean.class;
    }

    @Override
    public Boolean convert(String value) {
      if (value == null) {
        return null;
      }
      if (value.length() > 1
          && ((value.startsWith("'") && value.endsWith("'"))
              || (value.startsWith("\"") && value.endsWith("\"")))) {
        value = value.substring(1, value.length() - 1);
      }
      if ("true".equalsIgnoreCase(value)
          || "tru".equalsIgnoreCase(value)
          || "tr".equalsIgnoreCase(value)
          || "t".equalsIgnoreCase(value)
          || "on".equalsIgnoreCase(value)
          || "1".equalsIgnoreCase(value)
          || "yes".equalsIgnoreCase(value)
          || "ye".equalsIgnoreCase(value)
          || "y".equalsIgnoreCase(value)) {
        return Boolean.TRUE;
      }
      if ("false".equalsIgnoreCase(value)
          || "fals".equalsIgnoreCase(value)
          || "fal".equalsIgnoreCase(value)
          || "fa".equalsIgnoreCase(value)
          || "f".equalsIgnoreCase(value)
          || "off".equalsIgnoreCase(value)
          || "of".equalsIgnoreCase(value)
          || "0".equalsIgnoreCase(value)
          || "no".equalsIgnoreCase(value)
          || "n".equalsIgnoreCase(value)) {
        return Boolean.FALSE;
      }
      return null;
    }
  }

  /** Converter from string to a non-negative integer. */
  static class NonNegativeIntegerConverter implements ClientSideStatementValueConverter {

    public NonNegativeIntegerConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return Integer.class;
    }

    @Override
    public Integer convert(String value) {
      try {
        int res = Integer.parseInt(value);
        if (res < 0) {
          // The conventions for these converters is to return null if the value is invalid.
          return null;
        }
        return res;
      } catch (Exception ignore) {
        return null;
      }
    }
  }

  /** Converter from string to {@link Duration}. */
  static class DurationConverter implements ClientSideStatementValueConverter {
    private final String resetValue;

    private final Pattern allowedValues;

    public DurationConverter(String allowedValues) {
      this("NULL", allowedValues);
    }

    DurationConverter(String resetValue, String allowedValues) {
      this.resetValue = Preconditions.checkNotNull(resetValue);
      // Remove the parentheses from the beginning and end.
      this.allowedValues =
          Pattern.compile(
              "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z");
    }

    @Override
    public Class getParameterClass() {
      return Duration.class;
    }

    @Override
    public Duration convert(String value) {
      Matcher matcher = allowedValues.matcher(value);
      if (matcher.find()) {
        if (value.trim().equalsIgnoreCase(resetValue)) {
          return Duration.ZERO;
        } else {
          try {
            Duration duration;
            if (matcher.group(1) != null && matcher.group(2) != null) {
              ChronoUnit unit = toChronoUnit(parseTimeUnit(matcher.group(2)));
              duration = Duration.of(Long.parseLong(matcher.group(1)), unit);
            } else {
              duration = Duration.ofMillis(Long.parseLong(value.trim()));
            }
            if (duration.isZero()) {
              return null;
            }
            return duration;
          } catch (NumberFormatException exception) {
            // Converters should return null for invalid values.
            return null;
          }
        }
      }
      return null;
    }
  }

  /** Converter from string to {@link Duration}. */
  static class PgDurationConverter extends DurationConverter {
    public PgDurationConverter(String allowedValues) {
      super("DEFAULT", allowedValues);
    }
  }

  /** Converter from string to possible values for read only staleness ({@link TimestampBound}). */
  static class ReadOnlyStalenessConverter
      implements ClientSideStatementValueConverter {
    private final Pattern allowedValues;
    private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(Mode.class);

    public ReadOnlyStalenessConverter(String allowedValues) {
      // Remove the single quotes at the beginning and end.
      this.allowedValues =
          Pattern.compile(
              "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z");
    }

    @Override
    public Class getParameterClass() {
      return TimestampBound.class;
    }

    @Override
    public TimestampBound convert(String value) {
      Matcher matcher = allowedValues.matcher(value);
      if (matcher.find() && matcher.groupCount() >= 1) {
        Mode mode = null;
        int groupIndex = 0;
        for (int group = 1; group <= matcher.groupCount(); group++) {
          if (matcher.group(group) != null) {
            mode = values.get(matcher.group(group));
            if (mode != null) {
              groupIndex = group;
              break;
            }
          }
        }
        switch (mode) {
          case STRONG:
            return TimestampBound.strong();
          case READ_TIMESTAMP:
            return TimestampBound.ofReadTimestamp(
                ReadOnlyStalenessUtil.parseRfc3339(matcher.group(groupIndex + 1)));
          case MIN_READ_TIMESTAMP:
            return TimestampBound.ofMinReadTimestamp(
                ReadOnlyStalenessUtil.parseRfc3339(matcher.group(groupIndex + 1)));
          case EXACT_STALENESS:
            try {
              return TimestampBound.ofExactStaleness(
                  Long.parseLong(matcher.group(groupIndex + 2)),
                  parseTimeUnit(matcher.group(groupIndex + 3)));
            } catch (IllegalArgumentException e) {
              throw SpannerExceptionFactory.newSpannerException(
                  ErrorCode.INVALID_ARGUMENT, e.getMessage());
            }
          case MAX_STALENESS:
            try {
              return TimestampBound.ofMaxStaleness(
                  Long.parseLong(matcher.group(groupIndex + 2)),
                  parseTimeUnit(matcher.group(groupIndex + 3)));
            } catch (IllegalArgumentException e) {
              throw SpannerExceptionFactory.newSpannerException(
                  ErrorCode.INVALID_ARGUMENT, e.getMessage());
            }
          default:
            // fall through to allow the calling method to handle this
        }
      }
      return null;
    }
  }
  /**
   * Converter from string to possible values for {@link com.google.spanner.v1.DirectedReadOptions}.
   */
  static class DirectedReadOptionsConverter
      implements ClientSideStatementValueConverter {
    private final Pattern allowedValues;

    public DirectedReadOptionsConverter(String allowedValues) {
      // Remove the single quotes at the beginning and end.
      this.allowedValues =
          Pattern.compile(
              "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z");
    }

    @Override
    public Class getParameterClass() {
      return DirectedReadOptions.class;
    }

    @Override
    public DirectedReadOptions convert(String value) {
      Matcher matcher = allowedValues.matcher(value);
      if (matcher.find()) {
        try {
          return DirectedReadOptionsUtil.parse(value);
        } catch (SpannerException spannerException) {
          throw SpannerExceptionFactory.newSpannerException(
              ErrorCode.INVALID_ARGUMENT,
              String.format(
                  "Failed to parse '%s' as a valid value for DIRECTED_READ.\n"
                      + "The value should be a JSON string like this: '%s'.\n"
                      + "You can generate a valid JSON string from a DirectedReadOptions instance by calling %s.%s",
                  value,
                  "{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}",
                  DirectedReadOptionsUtil.class.getName(),
                  "toString(DirectedReadOptions directedReadOptions)"),
              spannerException);
        }
      }
      return null;
    }
  }

  /** Converter for converting strings to {@link AutocommitDmlMode} values. */
  static class AutocommitDmlModeConverter
      implements ClientSideStatementValueConverter {
    private final CaseInsensitiveEnumMap values =
        new CaseInsensitiveEnumMap<>(AutocommitDmlMode.class);

    public AutocommitDmlModeConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return AutocommitDmlMode.class;
    }

    @Override
    public AutocommitDmlMode convert(String value) {
      return values.get(value);
    }
  }

  static class StringValueConverter implements ClientSideStatementValueConverter {
    public StringValueConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return String.class;
    }

    @Override
    public String convert(String value) {
      return value;
    }
  }

  /** Converter for converting string values to {@link TransactionMode} values. */
  static class TransactionModeConverter
      implements ClientSideStatementValueConverter {
    private final CaseInsensitiveEnumMap values =
        new CaseInsensitiveEnumMap<>(TransactionMode.class, TransactionMode::getStatementString);

    public TransactionModeConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return TransactionMode.class;
    }

    @Override
    public TransactionMode convert(String value) {
      // Transaction mode may contain multiple spaces.
      String valueWithSingleSpaces = value.replaceAll("\\s+", " ");
      return values.get(valueWithSingleSpaces);
    }
  }

  static class PgTransactionIsolationConverter
      implements ClientSideStatementValueConverter {
    private final CaseInsensitiveEnumMap values =
        new CaseInsensitiveEnumMap<>(IsolationLevel.class, IsolationLevel::getShortStatementString);

    public PgTransactionIsolationConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return IsolationLevel.class;
    }

    @Override
    public IsolationLevel convert(String value) {
      // Isolation level may contain multiple spaces.
      String valueWithSingleSpaces = value.replaceAll("\\s+", " ");
      if (valueWithSingleSpaces.length() > 1
          && ((valueWithSingleSpaces.startsWith("'") && valueWithSingleSpaces.endsWith("'"))
              || (valueWithSingleSpaces.startsWith("\"")
                  && valueWithSingleSpaces.endsWith("\"")))) {
        valueWithSingleSpaces =
            valueWithSingleSpaces.substring(1, valueWithSingleSpaces.length() - 1);
      }
      return values.get(valueWithSingleSpaces);
    }
  }

  /**
   * Converter for converting string values to {@link PgTransactionMode} values. Includes no-op
   * handling of setting the isolation level of the transaction to default or serializable.
   */
  static class PgTransactionModeConverter
      implements ClientSideStatementValueConverter {
    PgTransactionModeConverter() {}

    public PgTransactionModeConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return PgTransactionMode.class;
    }

    @Override
    public PgTransactionMode convert(String value) {
      PgTransactionMode mode = new PgTransactionMode();
      // Transaction mode may contain multiple spaces.
      String valueWithoutDeferrable = value.replaceAll("(?i)(not\\s+deferrable)", " ");
      String valueWithSingleSpaces =
          valueWithoutDeferrable.replaceAll("\\s+", " ").toLowerCase(Locale.ENGLISH).trim();
      int currentIndex = 0;
      while (currentIndex < valueWithSingleSpaces.length()) {
        // This will use the last access mode and isolation level that is encountered in the string.
        // This is consistent with the behavior of PostgreSQL, which also allows multiple modes to
        // be specified in one string, and will use the last one that is encountered.
        if (valueWithSingleSpaces.substring(currentIndex).startsWith("read only")) {
          currentIndex += "read only".length();
          mode.setAccessMode(AccessMode.READ_ONLY_TRANSACTION);
        } else if (valueWithSingleSpaces.substring(currentIndex).startsWith("read write")) {
          currentIndex += "read write".length();
          mode.setAccessMode(AccessMode.READ_WRITE_TRANSACTION);
        } else if (valueWithSingleSpaces
            .substring(currentIndex)
            .startsWith("isolation level serializable")) {
          currentIndex += "isolation level serializable".length();
          mode.setIsolationLevel(IsolationLevel.ISOLATION_LEVEL_SERIALIZABLE);
        } else if (valueWithSingleSpaces
            .substring(currentIndex)
            .startsWith("isolation level default")) {
          currentIndex += "isolation level default".length();
          mode.setIsolationLevel(IsolationLevel.ISOLATION_LEVEL_DEFAULT);
        } else {
          return null;
        }
        // Skip space and/or comma that may separate multiple transaction modes.
        if (currentIndex < valueWithSingleSpaces.length()
            && valueWithSingleSpaces.charAt(currentIndex) == ' ') {
          currentIndex++;
        }
        if (currentIndex < valueWithSingleSpaces.length()
            && valueWithSingleSpaces.charAt(currentIndex) == ',') {
          currentIndex++;
        }
        if (currentIndex < valueWithSingleSpaces.length()
            && valueWithSingleSpaces.charAt(currentIndex) == ' ') {
          currentIndex++;
        }
      }
      return mode;
    }
  }

  /** Converter for converting strings to {@link RpcPriority} values. */
  static class RpcPriorityConverter implements ClientSideStatementValueConverter {
    private final CaseInsensitiveEnumMap values =
        new CaseInsensitiveEnumMap<>(RpcPriority.class);
    private final Pattern allowedValues;

    public RpcPriorityConverter(String allowedValues) {
      // Remove the parentheses from the beginning and end.
      this.allowedValues =
          Pattern.compile(
              "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z");
    }

    @Override
    public Class getParameterClass() {
      return RpcPriority.class;
    }

    @Override
    public RpcPriority convert(String value) {
      Matcher matcher = allowedValues.matcher(value);
      if (matcher.find()) {
        if (matcher.group(0).equalsIgnoreCase("null")) {
          return RpcPriority.UNSPECIFIED;
        }
      }
      return values.get(value);
    }
  }

  /** Converter for converting strings to {@link SavepointSupport} values. */
  static class SavepointSupportConverter
      implements ClientSideStatementValueConverter {
    private final CaseInsensitiveEnumMap values =
        new CaseInsensitiveEnumMap<>(SavepointSupport.class);

    public SavepointSupportConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return SavepointSupport.class;
    }

    @Override
    public SavepointSupport convert(String value) {
      return values.get(value);
    }
  }

  static class ExplainCommandConverter implements ClientSideStatementValueConverter {
    @Override
    public Class getParameterClass() {
      return String.class;
    }

    @Override
    public String convert(String value) {
      /* The first word in the string should be "explain"
       *  So, if the size of the string <= 7 (number of letters in the word "explain"), its an invalid statement
       *  If the size is greater than 7, we'll consider everything after explain as the query.
       */
      if (value.length() <= 7) {
        return null;
      }
      return value.substring(7).trim();
    }
  }

  /** Converter for converting Base64 encoded string to byte[] */
  static class ProtoDescriptorsConverter implements ClientSideStatementValueConverter {

    public ProtoDescriptorsConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return byte[].class;
    }

    @Override
    public byte[] convert(String value) {
      if (value == null || value.length() == 0 || value.equalsIgnoreCase("null")) {
        return null;
      }
      try {
        return Base64.getDecoder().decode(value);
      } catch (IllegalArgumentException e) {
        return null;
      }
    }
  }

  /** Converter for converting String that take in file path as input to String */
  static class ProtoDescriptorsFileConverter implements ClientSideStatementValueConverter {

    public ProtoDescriptorsFileConverter(String allowedValues) {}

    @Override
    public Class getParameterClass() {
      return String.class;
    }

    @Override
    public String convert(String filePath) {
      if (Strings.isNullOrEmpty(filePath)) {
        return null;
      }
      return filePath;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy