diff --git a/common/pom.xml b/common/pom.xml index 45e82ed..5e54bc4 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -32,8 +32,10 @@ dsbulk-codecs-api + com.datastax.oss dsbulk-codecs-text + 1.11.0-vectors-preview com.datastax.oss diff --git a/common/src/main/java/com/datastax/oss/common/sink/config/TopicConfig.java b/common/src/main/java/com/datastax/oss/common/sink/config/TopicConfig.java index 9b69e92..4a19343 100644 --- a/common/src/main/java/com/datastax/oss/common/sink/config/TopicConfig.java +++ b/common/src/main/java/com/datastax/oss/common/sink/config/TopicConfig.java @@ -16,6 +16,7 @@ package com.datastax.oss.common.sink.config; import com.datastax.oss.common.sink.ConfigException; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; import com.datastax.oss.driver.shaded.guava.common.base.Splitter; import com.datastax.oss.dsbulk.codecs.api.ConversionContext; import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; @@ -139,7 +140,7 @@ public String toString() { } @NonNull - public ConvertingCodecFactory createCodecFactory() { + public ConvertingCodecFactory createCodecFactory(DefaultCodecRegistry defaultCodecRegistry) { ConversionContext context = new TextConversionContext() .setLocale( @@ -150,7 +151,7 @@ public ConvertingCodecFactory createCodecFactory() { .setTimeZone(ZoneId.of(getString(getTopicSettingPath(topicName, TIMEZONE_OPT)))) .setTimeUnit( TimeUnit.valueOf(getString(getTopicSettingPath(topicName, TIME_UNIT_OPT)))); - return new ConvertingCodecFactory(context); + return new ConvertingCodecFactory(defaultCodecRegistry, context); } /** diff --git a/common/src/main/java/com/datastax/oss/common/sink/state/LifeCycleManager.java b/common/src/main/java/com/datastax/oss/common/sink/state/LifeCycleManager.java index c87d6bb..876e78a 100644 --- a/common/src/main/java/com/datastax/oss/common/sink/state/LifeCycleManager.java +++ b/common/src/main/java/com/datastax/oss/common/sink/state/LifeCycleManager.java @@ -53,16 +53,23 @@ import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.type.CqlVectorType; +import com.datastax.oss.driver.api.core.type.DataType; import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; import com.datastax.oss.driver.internal.core.auth.PlainTextAuthProvider; import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader; import com.datastax.oss.driver.internal.core.config.typesafe.DefaultProgrammaticDriverConfigLoaderBuilder; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.net.InetSocketAddress; import java.nio.file.Path; import java.util.Collection; @@ -89,6 +96,21 @@ public class LifeCycleManager { private static final ConcurrentMap INSTANCE_STATES = new ConcurrentHashMap<>(); private static MetricRegistry metricRegistry = new MetricRegistry(); + private static final DefaultCodecRegistry CODEC_REGISTRY = + new DefaultCodecRegistry("default-registry") { + + protected TypeCodec createCodec( + @Nullable DataType cqlType, + @Nullable GenericType javaType, + boolean isJavaCovariant) { + if (cqlType instanceof CqlVectorType) { + log.info("Automatically Registering codec for CqlVectorType {}", cqlType); + CqlVectorType vectorType = (CqlVectorType) cqlType; + return new CqlVectorCodec<>(vectorType, codecFor(vectorType.getSubtype())); + } + return super.createCodec(cqlType, javaType, isJavaCovariant); + } + }; /** This is a utility class that no one should instantiate. */ private LifeCycleManager() {} @@ -420,7 +442,8 @@ private static InstanceState buildInstanceState(CqlSession session, CassandraSin .stream() .map( topicConfig -> { - ConvertingCodecFactory codecFactory = topicConfig.createCodecFactory(); + ConvertingCodecFactory codecFactory = + topicConfig.createCodecFactory(CODEC_REGISTRY); TopicState topicState = new TopicState(codecFactory); topicStates.put(topicConfig.getTopicName(), topicState); @@ -485,6 +508,7 @@ public static CqlSession buildCqlSession( SslConfig sslConfig = config.getSslConfig(); CqlSessionBuilder builder = new SessionBuilder(sslConfig) + .withCodecRegistry(CODEC_REGISTRY) .withApplicationVersion(version) .withApplicationName(applicationName) .withClientId(generateClientId(config.getInstanceName())); diff --git a/common/src/test/java/com/datastax/oss/common/sink/RecordMapperTest.java b/common/src/test/java/com/datastax/oss/common/sink/RecordMapperTest.java index 8dcd36e..75c728b 100644 --- a/common/src/test/java/com/datastax/oss/common/sink/RecordMapperTest.java +++ b/common/src/test/java/com/datastax/oss/common/sink/RecordMapperTest.java @@ -59,11 +59,11 @@ import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.CqlTemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.ZonedTemporalFormat; import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; -import com.datastax.oss.dsbulk.codecs.api.util.CqlTemporalFormat; import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; -import com.datastax.oss.dsbulk.codecs.api.util.TemporalFormat; -import com.datastax.oss.dsbulk.codecs.api.util.ZonedTemporalFormat; import com.datastax.oss.dsbulk.codecs.text.string.StringToIntegerCodec; import com.datastax.oss.dsbulk.codecs.text.string.StringToLongCodec; import com.datastax.oss.protocol.internal.response.result.ColumnSpec; diff --git a/pom.xml b/pom.xml index 342d0fd..f3366aa 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ http://www.datastax.com + text common @@ -40,8 +41,8 @@ 8 2.4.0 2.6.2 - 4.6.0 - 1.6.0 + 4.16.0 + 1.10.0 1.0.3 25.1-jre 1.7.25 diff --git a/text/README.md b/text/README.md new file mode 100644 index 0000000..73e1073 --- /dev/null +++ b/text/README.md @@ -0,0 +1,4 @@ +# DataStax Bulk Loader Codecs - Text + +This module contains implementations of the ConvertingCodec API for Strings and Json. +Json conversion is done using FasterXML Jackson library. diff --git a/text/pom.xml b/text/pom.xml new file mode 100644 index 0000000..0a1e5e6 --- /dev/null +++ b/text/pom.xml @@ -0,0 +1,131 @@ + + + + 4.0.0 + + dsbulk-codecs + com.datastax.oss + 1.10.0 + + + 1.11.0-vectors-preview + dsbulk-codecs-text + DataStax Bulk Loader - Codecs - Text - Vectors preview + Text codecs for the DataStax Bulk Loader (String and JSON). + + + + com.datastax.oss + dsbulk-bom + 1.10.0 + pom + import + + + + + + com.datastax.oss + java-driver-core + 4.16.0 + + + com.datastax.oss + dsbulk-codecs-api + + + com.datastax.oss + java-driver-shaded-guava + + + org.slf4j + slf4j-api + + + io.netty + netty-common + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.datastax.oss + dsbulk-tests + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + ch.qos.logback + logback-classic + test + + + com.github.spotbugs + spotbugs-annotations + provided + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + javac + true + true + false + + -Werror + + 8 + 8 + 8 + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + + true + + + + + diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/TextConversionContext.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/TextConversionContext.java new file mode 100644 index 0000000..f4f42f4 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/TextConversionContext.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text; + +import com.datastax.oss.dsbulk.codecs.api.CommonConversionContext; +import com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; + +public class TextConversionContext extends CommonConversionContext { + + public static final String OBJECT_MAPPER = "OBJECT_MAPPER"; + + public TextConversionContext() { + addAttribute(OBJECT_MAPPER, JsonCodecUtils.getObjectMapper()); + } + + public TextConversionContext setObjectMapper(@NonNull ObjectMapper objectMapper) { + addAttribute(OBJECT_MAPPER, Objects.requireNonNull(objectMapper)); + return this; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonCodecUtils.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonCodecUtils.java new file mode 100644 index 0000000..790a336 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonCodecUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import java.math.BigDecimal; + +public class JsonCodecUtils { + + public static final GenericType JSON_NODE_TYPE = GenericType.of(JsonNode.class); + + /** + * A {@link JsonNodeFactory} that preserves {@link BigDecimal} scales, used to generate Json + * nodes. + */ + public static final JsonNodeFactory JSON_NODE_FACTORY = + JsonNodeFactory.withExactBigDecimals(true); + + /** + * The object mapper to use for converting Json nodes to and from Java types in Json codecs. + * + *

This is not the object mapper used by the Json connector to read and write Json files. + * + * @return The object mapper to use for converting Json nodes to and from Java types in Json + * codecs. + */ + public static ObjectMapper getObjectMapper() { + return JsonMapper.builder() + .nodeFactory(JSON_NODE_FACTORY) + // create a somewhat lenient mapper that recognizes a slightly relaxed Json syntax when + // parsing + .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) + .enable(JsonReadFeature.ALLOW_MISSING_VALUES) + .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS) + .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) + // fail on trailing tokens: the entire input must be parsed + .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeConvertingCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeConvertingCodec.java new file mode 100644 index 0000000..d0fa9e1 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeConvertingCodec.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.List; + +public abstract class JsonNodeConvertingCodec extends ConvertingCodec { + + private final List nullStrings; + + protected JsonNodeConvertingCodec(TypeCodec targetCodec, List nullStrings) { + super(targetCodec, JsonNode.class); + this.nullStrings = nullStrings; + } + + /** + * Whether the input is null. + * + *

This method should be used to inspect external inputs that are meant to be converted to + * textual CQL types only (text, varchar and ascii). + * + *

It always considers the empty string as NOT equivalent to NULL, unless the user clearly + * specifies that the empty string is to be considered as NULL, through the + * codec.nullStrings setting. + * + *

Do NOT use this method for non-textual CQL types; use {@link #isNullOrEmpty(JsonNode)} + * instead. + */ + protected boolean isNull(JsonNode node) { + return node == null + || node.isNull() + || node.isMissingNode() + || (node.isValueNode() && nullStrings.contains(node.asText())); + } + + /** + * Whether the input is null or empty. + * + *

This method should be used to inspect external inputs that are meant to be converted to + * non-textual CQL types only. + * + *

It always considers the empty string as equivalent to NULL, which is in compliance with the + * documentation of codec.nullStrings: "Note that, regardless of this setting, DSBulk + * will always convert empty strings to `null` if the target CQL type is not textual (i.e. not + * text, varchar or ascii)." + * + *

Do NOT use this method for textual CQL types; use {@link #isNull(JsonNode)} instead. + */ + protected boolean isNullOrEmpty(JsonNode node) { + return isNull(node) || (node.isValueNode() && node.asText().isEmpty()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeConvertingCodecProvider.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeConvertingCodecProvider.java new file mode 100644 index 0000000..442b06f --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeConvertingCodecProvider.java @@ -0,0 +1,429 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.api.CommonConversionContext.BINARY_FORMAT; +import static com.datastax.oss.dsbulk.codecs.api.CommonConversionContext.GEO_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.ALLOW_EXTRA_FIELDS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.ALLOW_MISSING_FIELDS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.BOOLEAN_INPUT_WORDS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.BOOLEAN_NUMBERS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.EPOCH; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.LOCAL_DATE_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.LOCAL_TIME_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.NULL_STRINGS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.NUMBER_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.OBJECT_MAPPER; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.OVERFLOW_STRATEGY; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.ROUNDING_MODE; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIMESTAMP_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIME_UNIT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIME_UUID_GENERATOR; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIME_ZONE; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.ASCII; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BIGINT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BLOB; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BOOLEAN; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.COUNTER; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.CUSTOM; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DATE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DECIMAL; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DOUBLE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DURATION; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.FLOAT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.INET; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.INT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.LIST; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.MAP; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.SET; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.SMALLINT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIME; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIMESTAMP; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIMEUUID; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TINYINT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TUPLE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.UDT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.UUID; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.VARCHAR; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.VARINT; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.CqlVectorType; +import com.datastax.oss.driver.api.core.type.CustomType; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecProvider; +import com.datastax.oss.dsbulk.codecs.text.json.dse.JsonNodeToDateRangeCodec; +import com.datastax.oss.dsbulk.codecs.text.json.dse.JsonNodeToLineStringCodec; +import com.datastax.oss.dsbulk.codecs.text.json.dse.JsonNodeToPointCodec; +import com.datastax.oss.dsbulk.codecs.text.json.dse.JsonNodeToPolygonCodec; +import com.fasterxml.jackson.databind.JsonNode; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A specialized {@link DefaultCodecRegistry} that is capable of producing {@link ConvertingCodec}s. + */ +public class JsonNodeConvertingCodecProvider implements ConvertingCodecProvider { + + private static final Logger LOGGER = + LoggerFactory.getLogger(JsonNodeConvertingCodecProvider.class); + + private static final String LINE_STRING_CLASS_NAME = + "org.apache.cassandra.db.marshal.LineStringType"; + private static final String POINT_CLASS_NAME = "org.apache.cassandra.db.marshal.PointType"; + private static final String POLYGON_CLASS_NAME = "org.apache.cassandra.db.marshal.PolygonType"; + private static final String DATE_RANGE_CLASS_NAME = + "org.apache.cassandra.db.marshal.DateRangeType"; + + @NonNull + @Override + public Optional> maybeProvide( + @NonNull DataType cqlType, + @NonNull GenericType externalJavaType, + @NonNull ConvertingCodecFactory codecFactory, + boolean rootCodec) { + if (!externalJavaType.equals(JSON_NODE_TYPE)) { + return Optional.empty(); + } + ConvertingCodec codec = + createJsonNodeConvertingCodec(cqlType, codecFactory, rootCodec); + return Optional.ofNullable(codec); + } + + @Nullable + private ConvertingCodec createJsonNodeConvertingCodec( + @NonNull DataType cqlType, @NonNull ConvertingCodecFactory codecFactory, boolean rootCodec) { + CodecRegistry codecRegistry = codecFactory.getCodecRegistry(); + ConversionContext context = codecFactory.getContext(); + // Don't apply null strings for non-root codecs + List nullStrings = rootCodec ? context.getAttribute(NULL_STRINGS) : ImmutableList.of(); + int cqlTypeCode = cqlType.getProtocolCode(); + switch (cqlTypeCode) { + case ASCII: + case VARCHAR: + TypeCodec typeCodec = codecRegistry.codecFor(cqlType); + return new JsonNodeToStringCodec( + typeCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + case BOOLEAN: + return new JsonNodeToBooleanCodec(context.getAttribute(BOOLEAN_INPUT_WORDS), nullStrings); + case TINYINT: + return new JsonNodeToByteCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case SMALLINT: + return new JsonNodeToShortCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case INT: + return new JsonNodeToIntegerCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case BIGINT: + return new JsonNodeToLongCodec( + TypeCodecs.BIGINT, + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case COUNTER: + return new JsonNodeToLongCodec( + TypeCodecs.COUNTER, + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case FLOAT: + return new JsonNodeToFloatCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case DOUBLE: + return new JsonNodeToDoubleCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case VARINT: + return new JsonNodeToBigIntegerCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case DECIMAL: + return new JsonNodeToBigDecimalCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case DATE: + return new JsonNodeToLocalDateCodec( + context.getAttribute(LOCAL_DATE_FORMAT), context.getAttribute(TIME_ZONE), nullStrings); + case TIME: + return new JsonNodeToLocalTimeCodec( + context.getAttribute(LOCAL_TIME_FORMAT), context.getAttribute(TIME_ZONE), nullStrings); + case TIMESTAMP: + return new JsonNodeToInstantCodec( + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(EPOCH), + nullStrings); + case INET: + return new JsonNodeToInetAddressCodec(nullStrings); + case UUID: + { + ConvertingCodec instantCodec = + codecFactory.createConvertingCodec(DataTypes.TIMESTAMP, GenericType.STRING, false); + return new JsonNodeToUUIDCodec( + TypeCodecs.UUID, + instantCodec, + context.getAttribute(TIME_UUID_GENERATOR), + nullStrings); + } + case TIMEUUID: + { + ConvertingCodec instantCodec = + codecFactory.createConvertingCodec(DataTypes.TIMESTAMP, GenericType.STRING, false); + return new JsonNodeToUUIDCodec( + TypeCodecs.TIMEUUID, + instantCodec, + context.getAttribute(TIME_UUID_GENERATOR), + nullStrings); + } + case BLOB: + return new JsonNodeToBlobCodec(context.getAttribute(BINARY_FORMAT), nullStrings); + case DURATION: + return new JsonNodeToDurationCodec(nullStrings); + case LIST: + { + DataType elementType = ((ListType) cqlType).getElementType(); + TypeCodec> collectionCodec = codecRegistry.codecFor(cqlType); + @SuppressWarnings("unchecked") + ConvertingCodec eltCodec = + (ConvertingCodec) + createJsonNodeConvertingCodec(elementType, codecFactory, false); + return new JsonNodeToListCodec<>( + collectionCodec, eltCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case SET: + { + DataType elementType = ((SetType) cqlType).getElementType(); + TypeCodec> collectionCodec = codecRegistry.codecFor(cqlType); + @SuppressWarnings("unchecked") + ConvertingCodec eltCodec = + (ConvertingCodec) + createJsonNodeConvertingCodec(elementType, codecFactory, false); + return new JsonNodeToSetCodec<>( + collectionCodec, eltCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case MAP: + { + DataType keyType = ((MapType) cqlType).getKeyType(); + DataType valueType = ((MapType) cqlType).getValueType(); + TypeCodec> mapCodec = codecRegistry.codecFor(cqlType); + ConvertingCodec keyCodec = + codecFactory.createConvertingCodec(keyType, GenericType.STRING, false); + ConvertingCodec valueCodec = + codecFactory.createConvertingCodec(valueType, JSON_NODE_TYPE, false); + return new JsonNodeToMapCodec<>( + mapCodec, keyCodec, valueCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case TUPLE: + { + TypeCodec tupleCodec = codecRegistry.codecFor(cqlType); + ImmutableList.Builder> eltCodecs = + new ImmutableList.Builder<>(); + for (DataType eltType : ((TupleType) cqlType).getComponentTypes()) { + ConvertingCodec eltCodec = + codecFactory.createConvertingCodec(eltType, JSON_NODE_TYPE, false); + eltCodecs.add(Objects.requireNonNull(eltCodec)); + } + return new JsonNodeToTupleCodec( + tupleCodec, + eltCodecs.build(), + context.getAttribute(OBJECT_MAPPER), + nullStrings, + context.getAttribute(ALLOW_EXTRA_FIELDS), + context.getAttribute(ALLOW_MISSING_FIELDS)); + } + case UDT: + { + TypeCodec udtCodec = codecRegistry.codecFor(cqlType); + ImmutableMap.Builder> fieldCodecs = + new ImmutableMap.Builder<>(); + List fieldNames = ((UserDefinedType) cqlType).getFieldNames(); + List fieldTypes = ((UserDefinedType) cqlType).getFieldTypes(); + assert (fieldNames.size() == fieldTypes.size()); + + for (int idx = 0; idx < fieldNames.size(); idx++) { + CqlIdentifier fieldName = fieldNames.get(idx); + DataType fieldType = fieldTypes.get(idx); + ConvertingCodec fieldCodec = + codecFactory.createConvertingCodec(fieldType, JSON_NODE_TYPE, false); + fieldCodecs.put(fieldName, Objects.requireNonNull(fieldCodec)); + } + return new JsonNodeToUDTCodec( + udtCodec, + fieldCodecs.build(), + context.getAttribute(OBJECT_MAPPER), + nullStrings, + context.getAttribute(ALLOW_EXTRA_FIELDS), + context.getAttribute(ALLOW_MISSING_FIELDS)); + } + case CUSTOM: + { + CustomType customType = (CustomType) cqlType; + switch (customType.getClassName()) { + case POINT_CLASS_NAME: + return new JsonNodeToPointCodec( + context.getAttribute(OBJECT_MAPPER), + context.getAttribute(GEO_FORMAT), + nullStrings); + case LINE_STRING_CLASS_NAME: + return new JsonNodeToLineStringCodec( + context.getAttribute(OBJECT_MAPPER), + context.getAttribute(GEO_FORMAT), + nullStrings); + case POLYGON_CLASS_NAME: + return new JsonNodeToPolygonCodec( + context.getAttribute(OBJECT_MAPPER), + context.getAttribute(GEO_FORMAT), + nullStrings); + case DATE_RANGE_CLASS_NAME: + return new JsonNodeToDateRangeCodec(nullStrings); + case CqlVectorType.CQLVECTOR_CLASS_NAME: + CqlVectorType cqlVectorType = (CqlVectorType) cqlType; + // Step 1: create a JSON codec which will take the input JSON nodes and generate + // something matching the expected data type + ConvertingCodec jsonCodec = + createJsonNodeConvertingCodec(cqlVectorType.getSubtype(), codecFactory, false); + // Step 2: create a conventional codec which will take instances of the Java type + // generated by the JSON codec above and perform standard serde on them. + ConvertingCodec standardCodec = + codecFactory.createConvertingCodec( + cqlVectorType.getSubtype(), jsonCodec.getInternalJavaType(), false); + return new JsonNodeToVectorCodec( + new CqlVectorCodec(cqlVectorType, standardCodec), + jsonCodec, + context.getAttribute(OBJECT_MAPPER), + nullStrings); + } + } + // fall through + default: + try { + TypeCodec innerCodec = codecRegistry.codecFor(cqlType); + LOGGER.warn( + String.format( + "CQL type %s is not officially supported by this version of DSBulk; " + + "JSON literals will be parsed and formatted using registered codec %s", + cqlType, innerCodec.getClass().getSimpleName())); + return new JsonNodeToUnknownTypeCodec<>(innerCodec, nullStrings); + } catch (CodecNotFoundException ignored) { + } + return null; + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigDecimalCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigDecimalCodec.java new file mode 100644 index 0000000..8cacd9c --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigDecimalCodec.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +// import static com.datastax.oss.dsbulk.commons.codecs.json.JsonCodecUtils.JSON_NODE_FACTORY; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToBigDecimalCodec extends JsonNodeToNumberCodec { + + public JsonNodeToBigDecimalCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.DECIMAL, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers, + nullStrings); + } + + @Override + public BigDecimal externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isBigDecimal()) { + return node.decimalValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + if (number == null) { + return null; + } + return CodecUtils.toBigDecimal(number); + } + + @Override + public JsonNode internalToExternal(BigDecimal value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigIntegerCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigIntegerCodec.java new file mode 100644 index 0000000..9c9914b --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigIntegerCodec.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToBigIntegerCodec extends JsonNodeToNumberCodec { + + public JsonNodeToBigIntegerCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.VARINT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::toBigInteger).collect(toList()), + nullStrings); + } + + @Override + public BigInteger externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isBigInteger()) { + return node.bigIntegerValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + return narrowNumber(number, BigInteger.class); + } + + @Override + public JsonNode internalToExternal(BigInteger value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBlobCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBlobCodec.java new file mode 100644 index 0000000..a82b910 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBlobCodec.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.binary.BinaryFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +public class JsonNodeToBlobCodec extends JsonNodeConvertingCodec { + + private final BinaryFormat binaryFormat; + + public JsonNodeToBlobCodec(BinaryFormat binaryFormat, List nullStrings) { + super(TypeCodecs.BLOB, nullStrings); + this.binaryFormat = binaryFormat; + } + + @Override + public ByteBuffer externalToInternal(JsonNode node) { + // Do not test isNullOrEmpty(), it returns true for empty binary nodes + if (isNull(node)) { + return null; + } + if (node.isBinary()) { + try { + return ByteBuffer.wrap(node.binaryValue()); + } catch (IOException ignored) { + // try as a string below + } + } + String s = node.asText(); + return CodecUtils.parseByteBuffer(s); + } + + @Override + public JsonNode internalToExternal(ByteBuffer value) { + return JsonCodecUtils.JSON_NODE_FACTORY.textNode(binaryFormat.format(value)); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBooleanCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBooleanCodec.java new file mode 100644 index 0000000..6f1efe7 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBooleanCodec.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.List; +import java.util.Map; + +public class JsonNodeToBooleanCodec extends JsonNodeConvertingCodec { + + private final Map inputs; + + public JsonNodeToBooleanCodec(Map inputs, List nullStrings) { + super(TypeCodecs.BOOLEAN, nullStrings); + this.inputs = inputs; + } + + @Override + public Boolean externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isBoolean()) { + return node.asBoolean(); + } + String s = node.asText(); + Boolean b = inputs.get(s.toLowerCase()); + if (b == null) { + throw new IllegalArgumentException("Invalid boolean value: " + s); + } + return b; + } + + @Override + public JsonNode internalToExternal(Boolean value) { + return value == null ? null : JSON_NODE_FACTORY.booleanNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToByteCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToByteCodec.java new file mode 100644 index 0000000..b596ef0 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToByteCodec.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToByteCodec extends JsonNodeToNumberCodec { + + public JsonNodeToByteCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.TINYINT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::byteValueExact).collect(toList()), + nullStrings); + } + + @Override + public Byte externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + if (number == null) { + return null; + } + return narrowNumber(number, Byte.class); + } + + @Override + public JsonNode internalToExternal(Byte value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToCollectionCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToCollectionCodec.java new file mode 100644 index 0000000..ba870c5 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToCollectionCodec.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +public abstract class JsonNodeToCollectionCodec> + extends JsonNodeConvertingCodec { + + private final ConvertingCodec eltCodec; + private final Supplier collectionSupplier; + private final ObjectMapper objectMapper; + private final C emptyCollection; + + JsonNodeToCollectionCodec( + TypeCodec collectionCodec, + ConvertingCodec eltCodec, + ObjectMapper objectMapper, + Supplier collectionSupplier, + List nullStrings, + C emptyCollection) { + super(collectionCodec, nullStrings); + this.eltCodec = eltCodec; + this.objectMapper = objectMapper; + this.collectionSupplier = collectionSupplier; + this.emptyCollection = emptyCollection; + } + + @Override + public C externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (!node.isArray()) { + throw new IllegalArgumentException("Expecting ARRAY node, got " + node.getNodeType()); + } + if (node.size() == 0) { + return emptyCollection; + } + Iterator elements = node.elements(); + C collection = collectionSupplier.get(); + while (elements.hasNext()) { + JsonNode element = elements.next(); + collection.add(eltCodec.externalToInternal(element)); + } + return collection; + } + + @Override + public JsonNode internalToExternal(C value) { + if (value == null) { + return null; + } + ArrayNode root = objectMapper.createArrayNode(); + for (E element : value) { + root.add(eltCodec.internalToExternal(element)); + } + return root; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDoubleCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDoubleCodec.java new file mode 100644 index 0000000..047aca3 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDoubleCodec.java @@ -0,0 +1,85 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToDoubleCodec extends JsonNodeToNumberCodec { + + public JsonNodeToDoubleCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.DOUBLE, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::doubleValue).collect(toList()), + nullStrings); + } + + @Override + public Double externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isDouble()) { + return node.doubleValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + if (number == null) { + return null; + } + return narrowNumber(number, Double.class); + } + + @Override + public JsonNode internalToExternal(Double value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDurationCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDurationCodec.java new file mode 100644 index 0000000..4ba50d3 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDurationCodec.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.List; + +public class JsonNodeToDurationCodec extends JsonNodeConvertingCodec { + + public JsonNodeToDurationCodec(List nullStrings) { + super(TypeCodecs.DURATION, nullStrings); + } + + @Override + public CqlDuration externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + String s = node.asText(); + return CqlDuration.from(s); + } + + @Override + public JsonNode internalToExternal(CqlDuration value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.textNode(value.toString()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToFloatCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToFloatCodec.java new file mode 100644 index 0000000..e251032 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToFloatCodec.java @@ -0,0 +1,85 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToFloatCodec extends JsonNodeToNumberCodec { + + public JsonNodeToFloatCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.FLOAT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::floatValue).collect(toList()), + nullStrings); + } + + @Override + public Float externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isFloat()) { + return node.floatValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + if (number == null) { + return null; + } + return narrowNumber(number, Float.class); + } + + @Override + public JsonNode internalToExternal(Float value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInetAddressCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInetAddressCodec.java new file mode 100644 index 0000000..1fcedfa --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInetAddressCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.fasterxml.jackson.databind.JsonNode; +import java.net.InetAddress; +import java.util.List; + +public class JsonNodeToInetAddressCodec extends JsonNodeConvertingCodec { + + public JsonNodeToInetAddressCodec(List nullStrings) { + super(TypeCodecs.INET, nullStrings); + } + + @Override + public InetAddress externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + String s = node.asText(); + if (s.isEmpty()) { + throw new IllegalArgumentException("Cannot create inet address from empty string"); + } + try { + return InetAddress.getByName(s); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot parse inet address: " + s); + } + } + + @Override + public JsonNode internalToExternal(InetAddress value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.textNode(value.getHostAddress()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInstantCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInstantCodec.java new file mode 100644 index 0000000..43f2363 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInstantCodec.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public class JsonNodeToInstantCodec extends JsonNodeToTemporalCodec { + + private final ZoneId timeZone; + private final ZonedDateTime epoch; + + public JsonNodeToInstantCodec( + TemporalFormat temporalFormat, + ZoneId timeZone, + ZonedDateTime epoch, + List nullStrings) { + super(TypeCodecs.TIMESTAMP, temporalFormat, nullStrings); + this.timeZone = timeZone; + this.epoch = epoch; + } + + @Override + public Instant externalToInternal(JsonNode node) { + TemporalAccessor temporal = parseTemporalAccessor(node); + if (temporal == null) { + return null; + } + return CodecUtils.toInstant(temporal, timeZone, epoch.toLocalDate()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToIntegerCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToIntegerCodec.java new file mode 100644 index 0000000..3257910 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToIntegerCodec.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToIntegerCodec extends JsonNodeToNumberCodec { + + public JsonNodeToIntegerCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.INT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::intValueExact).collect(toList()), + nullStrings); + } + + @Override + public Integer externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isInt()) { + return node.intValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + return narrowNumber(number, Integer.class); + } + + @Override + public JsonNode internalToExternal(Integer value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToListCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToListCodec.java new file mode 100644 index 0000000..ef6b8a9 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToListCodec.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; + +public class JsonNodeToListCodec extends JsonNodeToCollectionCodec> { + + public JsonNodeToListCodec( + TypeCodec> collectionCodec, + ConvertingCodec eltCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(collectionCodec, eltCodec, objectMapper, ArrayList::new, nullStrings, ImmutableList.of()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalDateCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalDateCodec.java new file mode 100644 index 0000000..1732793 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalDateCodec.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public class JsonNodeToLocalDateCodec extends JsonNodeToTemporalCodec { + + private final ZoneId timeZone; + + public JsonNodeToLocalDateCodec( + TemporalFormat parser, ZoneId timeZone, List nullStrings) { + super(TypeCodecs.DATE, parser, nullStrings); + this.timeZone = timeZone; + } + + @Override + public LocalDate externalToInternal(JsonNode node) { + TemporalAccessor temporal = parseTemporalAccessor(node); + if (temporal == null) { + return null; + } + return CodecUtils.toLocalDate(temporal, timeZone); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalTimeCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalTimeCodec.java new file mode 100644 index 0000000..faafe4c --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalTimeCodec.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public class JsonNodeToLocalTimeCodec extends JsonNodeToTemporalCodec { + + private final ZoneId timeZone; + + public JsonNodeToLocalTimeCodec( + TemporalFormat parser, ZoneId timeZone, List nullStrings) { + super(TypeCodecs.TIME, parser, nullStrings); + this.timeZone = timeZone; + } + + @Override + public LocalTime externalToInternal(JsonNode s) { + TemporalAccessor temporal = parseTemporalAccessor(s); + if (temporal == null) { + return null; + } + return CodecUtils.toLocalTime(temporal, timeZone); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLongCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLongCodec.java new file mode 100644 index 0000000..eea7b0a --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLongCodec.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.PrimitiveLongCodec; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToLongCodec extends JsonNodeToNumberCodec { + + public JsonNodeToLongCodec( + PrimitiveLongCodec targetCodec, + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + targetCodec, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::longValueExact).collect(toList()), + nullStrings); + } + + @Override + public Long externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isLong()) { + return node.longValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + return narrowNumber(number, Long.class); + } + + @Override + public JsonNode internalToExternal(Long value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToMapCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToMapCodec.java new file mode 100644 index 0000000..1ff573b --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToMapCodec.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class JsonNodeToMapCodec extends JsonNodeConvertingCodec> { + + private final ConvertingCodec keyCodec; + private final ConvertingCodec valueCodec; + private final ObjectMapper objectMapper; + private final Map emptyMap; + + public JsonNodeToMapCodec( + TypeCodec> collectionCodec, + ConvertingCodec keyCodec, + ConvertingCodec valueCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(collectionCodec, nullStrings); + this.keyCodec = keyCodec; + this.valueCodec = valueCodec; + this.objectMapper = objectMapper; + emptyMap = ImmutableMap.of(); + } + + @Override + public Map externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (!node.isObject()) { + throw new IllegalArgumentException("Expecting OBJECT node, got " + node.getNodeType()); + } + if (node.size() == 0) { + return emptyMap; + } + Map map = new LinkedHashMap<>(node.size()); + Iterator> it = node.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + map.put( + keyCodec.externalToInternal(entry.getKey()), + valueCodec.externalToInternal(entry.getValue())); + } + return map; + } + + @Override + public JsonNode internalToExternal(Map map) { + if (map == null) { + return null; + } + ObjectNode root = objectMapper.createObjectNode(); + for (Map.Entry entry : map.entrySet()) { + String key = keyCodec.internalToExternal(entry.getKey()); + JsonNode value = valueCodec.internalToExternal(entry.getValue()); + root.set(key, value); + } + return root; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToNumberCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToNumberCodec.java new file mode 100644 index 0000000..dbad3e0 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToNumberCodec.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +abstract class JsonNodeToNumberCodec extends JsonNodeConvertingCodec { + + private final FastThreadLocal numberFormat; + private final OverflowStrategy overflowStrategy; + private final RoundingMode roundingMode; + private final TemporalFormat temporalFormat; + private final ZoneId timeZone; + private final TimeUnit timeUnit; + private final ZonedDateTime epoch; + private final Map booleanStrings; + private final List booleanNumbers; + + JsonNodeToNumberCodec( + TypeCodec targetCodec, + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super(targetCodec, nullStrings); + this.numberFormat = numberFormat; + this.overflowStrategy = overflowStrategy; + this.roundingMode = roundingMode; + this.temporalFormat = temporalFormat; + this.timeZone = timeZone; + this.timeUnit = timeUnit; + this.epoch = epoch; + this.booleanStrings = booleanStrings; + this.booleanNumbers = booleanNumbers; + } + + Number parseNumber(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + return CodecUtils.parseNumber( + node.asText(), + numberFormat.get(), + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers); + } + + N narrowNumber(Number number, Class targetClass) { + return CodecUtils.narrowNumber(number, targetClass, overflowStrategy, roundingMode); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToSetCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToSetCodec.java new file mode 100644 index 0000000..ebee816 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToSetCodec.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class JsonNodeToSetCodec extends JsonNodeToCollectionCodec> { + + public JsonNodeToSetCodec( + TypeCodec> collectionCodec, + ConvertingCodec eltCodec, + ObjectMapper objectMapper, + List nullStrings) { + super( + collectionCodec, + eltCodec, + objectMapper, + LinkedHashSet::new, + nullStrings, + ImmutableSet.of()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToShortCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToShortCodec.java new file mode 100644 index 0000000..44a0cef --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToShortCodec.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class JsonNodeToShortCodec extends JsonNodeToNumberCodec { + + public JsonNodeToShortCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.SMALLINT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::shortValueExact).collect(toList()), + nullStrings); + } + + @Override + public Short externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node.isShort()) { + return node.shortValue(); + } + Number number; + if (node.isNumber()) { + number = node.numberValue(); + } else { + number = parseNumber(node); + } + return narrowNumber(number, Short.class); + } + + @Override + public JsonNode internalToExternal(Short value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.numberNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToStringCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToStringCodec.java new file mode 100644 index 0000000..77bd7fd --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToStringCodec.java @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; + +public class JsonNodeToStringCodec extends JsonNodeConvertingCodec { + + private final ObjectMapper objectMapper; + + public JsonNodeToStringCodec( + TypeCodec innerCodec, ObjectMapper objectMapper, List nullStrings) { + super(innerCodec, nullStrings); + this.objectMapper = objectMapper; + } + + @Override + public String externalToInternal(JsonNode node) { + // DAT-297: do not convert empty strings to null so do not use isNullOrEmpty() here + if (isNull(node)) { + return null; + } + if (node.isContainerNode()) { + try { + return objectMapper.writeValueAsString(node); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Cannot deserialize node " + node, e); + } + } else { + return node.asText(); + } + } + + @Override + public JsonNode internalToExternal(String value) { + if (value == null) { + return null; + } + return objectMapper.getNodeFactory().textNode(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTemporalCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTemporalCodec.java new file mode 100644 index 0000000..ae869a3 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTemporalCodec.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.NumericTemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public abstract class JsonNodeToTemporalCodec + extends JsonNodeConvertingCodec { + + final TemporalFormat temporalFormat; + + JsonNodeToTemporalCodec( + TypeCodec targetCodec, TemporalFormat temporalFormat, List nullStrings) { + super(targetCodec, nullStrings); + this.temporalFormat = temporalFormat; + } + + @Override + public JsonNode internalToExternal(T value) { + if (value == null) { + return null; + } else if (temporalFormat instanceof NumericTemporalFormat) { + Number n = ((NumericTemporalFormat) temporalFormat).temporalToNumber(value); + if (n == null) { + return null; + } + if (n instanceof Byte) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((Byte) n); + } + if (n instanceof Short) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((Short) n); + } + if (n instanceof Integer) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((Integer) n); + } + if (n instanceof Long) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((Long) n); + } + if (n instanceof Float) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((Float) n); + } + if (n instanceof Double) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((Double) n); + } + if (n instanceof BigInteger) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((BigInteger) n); + } + if (n instanceof BigDecimal) { + return JsonCodecUtils.JSON_NODE_FACTORY.numberNode((BigDecimal) n); + } + return JsonCodecUtils.JSON_NODE_FACTORY.textNode(n.toString()); + } else { + return JsonCodecUtils.JSON_NODE_FACTORY.textNode(temporalFormat.format(value)); + } + } + + TemporalAccessor parseTemporalAccessor(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (node instanceof NumericNode && temporalFormat instanceof NumericTemporalFormat) { + Number n = node.numberValue(); + return ((NumericTemporalFormat) temporalFormat).numberToTemporal(n); + } else { + String s = node.asText(); + return temporalFormat.parse(s); + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTupleCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTupleCodec.java new file mode 100644 index 0000000..09c55da --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTupleCodec.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import java.util.List; + +public class JsonNodeToTupleCodec extends JsonNodeConvertingCodec { + + private final TupleType definition; + private final List> eltCodecs; + private final ObjectMapper objectMapper; + private final boolean allowExtraFields; + private final boolean allowMissingFields; + + public JsonNodeToTupleCodec( + TypeCodec tupleCodec, + List> eltCodecs, + ObjectMapper objectMapper, + List nullStrings, + boolean allowExtraFields, + boolean allowMissingFields) { + super(tupleCodec, nullStrings); + this.eltCodecs = eltCodecs; + definition = (TupleType) tupleCodec.getCqlType(); + this.objectMapper = objectMapper; + this.allowExtraFields = allowExtraFields; + this.allowMissingFields = allowMissingFields; + } + + @Override + public TupleValue externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (!node.isArray()) { + throw new IllegalArgumentException("Expecting ARRAY node, got " + node.getNodeType()); + } + int tupleSize = definition.getComponentTypes().size(); + int nodeSize = node.size(); + if (nodeSize > tupleSize && !allowExtraFields) { + throw JsonSchemaMismatchException.arraySizeGreaterThanTupleSize(tupleSize, nodeSize); + } + if (nodeSize < tupleSize && !allowMissingFields) { + throw JsonSchemaMismatchException.arraySizeLesserThanTupleSize(tupleSize, nodeSize); + } + TupleValue tuple = definition.newValue(); + for (int i = 0; i < tupleSize && i < nodeSize; i++) { + ConvertingCodec eltCodec = eltCodecs.get(i); + Object o = eltCodec.externalToInternal(node.get(i)); + tuple = tuple.set(i, o, eltCodec.getInternalJavaType()); + } + return tuple; + } + + @Override + public JsonNode internalToExternal(TupleValue tuple) { + if (tuple == null) { + return null; + } + ArrayNode root = objectMapper.createArrayNode(); + int size = definition.getComponentTypes().size(); + for (int i = 0; i < size; i++) { + ConvertingCodec eltCodec = eltCodecs.get(i); + Object o = tuple.get(i, eltCodec.getInternalJavaType()); + JsonNode element = eltCodec.internalToExternal(o); + root.add(element); + } + return root; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUDTCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUDTCodec.java new file mode 100644 index 0000000..bc2d69e --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUDTCodec.java @@ -0,0 +1,143 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class JsonNodeToUDTCodec extends JsonNodeConvertingCodec { + + private final Map> fieldCodecs; + private final UserDefinedType definition; + private final ObjectMapper objectMapper; + private final boolean allowExtraFields; + private final boolean allowMissingFields; + + public JsonNodeToUDTCodec( + TypeCodec udtCodec, + Map> fieldCodecs, + ObjectMapper objectMapper, + List nullStrings, + boolean allowExtraFields, + boolean allowMissingFields) { + super(udtCodec, nullStrings); + this.fieldCodecs = fieldCodecs; + definition = (UserDefinedType) udtCodec.getCqlType(); + this.objectMapper = objectMapper; + this.allowExtraFields = allowExtraFields; + this.allowMissingFields = allowMissingFields; + } + + @Override + public UdtValue externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + if (!(node.isObject() || node.isArray())) { + throw new IllegalArgumentException( + "Expecting OBJECT or ARRAY node, got " + node.getNodeType()); + } + if (node.size() == 0 && allowMissingFields) { + return definition.newValue(); + } + UdtValue value = definition.newValue(); + if (node.isObject()) { + checkJsonObject(node); + for (CqlIdentifier field : definition.getFieldNames()) { + String name = field.asInternal(); + if (node.has(name)) { + ConvertingCodec fieldCodec = fieldCodecs.get(field); + Object o = fieldCodec.externalToInternal(node.get(name)); + value = value.set(field, o, fieldCodec.getInternalJavaType()); + } + } + } else { + checkJsonArray(node); + // The field iteration order is deterministic + Iterator fields = definition.getFieldNames().iterator(); + for (int i = 0; i < node.size() && fields.hasNext(); i++) { + JsonNode element = node.get(i); + CqlIdentifier field = fields.next(); + ConvertingCodec fieldCodec = fieldCodecs.get(field); + Object o = fieldCodec.externalToInternal(element); + value = value.set(i, o, fieldCodec.getInternalJavaType()); + } + } + return value; + } + + private void checkJsonObject(JsonNode node) { + Set udtFieldNames = + definition.getFieldNames().stream() + .map(CqlIdentifier::asInternal) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Set nodeFieldNames = new LinkedHashSet<>(); + Iterators.addAll(nodeFieldNames, node.fieldNames()); + if (!udtFieldNames.equals(nodeFieldNames)) { + Set extraneous = Sets.difference(nodeFieldNames, udtFieldNames); + Set missing = Sets.difference(udtFieldNames, nodeFieldNames); + boolean hasExtras = !allowExtraFields && !extraneous.isEmpty(); + boolean hasMissing = !allowMissingFields && !missing.isEmpty(); + if (hasMissing && hasExtras) { + throw JsonSchemaMismatchException.objectHasMissingAndExtraneousFields(extraneous, missing); + } else if (hasExtras) { + throw JsonSchemaMismatchException.objectHasExtraneousFields(extraneous); + } else if (hasMissing) { + throw JsonSchemaMismatchException.objectHasMissingFields(missing); + } + } + } + + private void checkJsonArray(JsonNode node) { + int udtSize = definition.getFieldNames().size(); + int nodeSize = node.size(); + if (nodeSize > udtSize && !allowExtraFields) { + throw JsonSchemaMismatchException.arraySizeGreaterThanUDTSize(udtSize, nodeSize); + } + if (nodeSize < udtSize && !allowMissingFields) { + throw JsonSchemaMismatchException.arraySizeLesserThanUDTSize(udtSize, nodeSize); + } + } + + @Override + public JsonNode internalToExternal(UdtValue value) { + if (value == null) { + return null; + } + ObjectNode root = objectMapper.createObjectNode(); + for (CqlIdentifier field : definition.getFieldNames()) { + ConvertingCodec eltCodec = fieldCodecs.get(field); + Object o = value.get(field, eltCodec.getInternalJavaType()); + JsonNode node = eltCodec.internalToExternal(o); + root.set(field.asInternal(), node); + } + return root; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUUIDCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUUIDCodec.java new file mode 100644 index 0000000..735e9dc --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUUIDCodec.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.TimeUUIDGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public class JsonNodeToUUIDCodec extends JsonNodeConvertingCodec { + + private final ConvertingCodec instantCodec; + private final TimeUUIDGenerator generator; + + public JsonNodeToUUIDCodec( + TypeCodec targetCodec, + ConvertingCodec instantCodec, + TimeUUIDGenerator generator, + List nullStrings) { + super(targetCodec, nullStrings); + this.instantCodec = instantCodec; + this.generator = generator; + } + + @Override + public UUID externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + return CodecUtils.parseUUID(node.asText(), instantCodec, generator); + } + + @Override + public JsonNode internalToExternal(UUID value) { + return value == null ? null : JsonCodecUtils.JSON_NODE_FACTORY.textNode(value.toString()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUnknownTypeCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUnknownTypeCodec.java new file mode 100644 index 0000000..de52429 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUnknownTypeCodec.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.List; + +public class JsonNodeToUnknownTypeCodec extends JsonNodeConvertingCodec { + + public JsonNodeToUnknownTypeCodec(TypeCodec targetCodec, List nullStrings) { + super(targetCodec, nullStrings); + } + + @Override + public T externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + return getInternalCodec().parse(node.asText()); + } + + @Override + public JsonNode internalToExternal(T o) { + if (o == null) { + return null; + } + String s = getInternalCodec().format(o); + // most codecs usually format null/empty values using the CQL keyword "NULL", + // but some others may choose to return a null string. + if (s.equalsIgnoreCase("NULL")) { + return null; + } + return JSON_NODE_FACTORY.textNode(s); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToVectorCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToVectorCodec.java new file mode 100644 index 0000000..b0a7f8c --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToVectorCodec.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import com.datastax.oss.driver.api.core.data.CqlVector; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import java.util.Iterator; +import java.util.List; + +public class JsonNodeToVectorCodec extends JsonNodeConvertingCodec> { + + private final ConvertingCodec subtypeCodec; + private final ObjectMapper objectMapper; + + public JsonNodeToVectorCodec( + CqlVectorCodec targetCodec, + ConvertingCodec subtypeCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(targetCodec, nullStrings); + this.subtypeCodec = subtypeCodec; + this.objectMapper = objectMapper; + } + + @Override + public CqlVector externalToInternal(JsonNode jsonNode) { + if (jsonNode == null || !jsonNode.isArray()) return null; + CqlVector.Builder builder = CqlVector.builder(); + for (Iterator it = jsonNode.elements(); it.hasNext(); ) + builder.add(subtypeCodec.externalToInternal(it.next())); + return builder.build(); + } + + @Override + public JsonNode internalToExternal(CqlVector value) { + if (value == null) return null; + ArrayNode root = objectMapper.createArrayNode(); + for (SubtypeT element : value.getValues()) { + root.add(subtypeCodec.internalToExternal(element)); + } + return root; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonSchemaMismatchException.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonSchemaMismatchException.java new file mode 100644 index 0000000..4b6cfd8 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/JsonSchemaMismatchException.java @@ -0,0 +1,125 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Set; + +class JsonSchemaMismatchException extends IllegalArgumentException { + + private JsonSchemaMismatchException(String message) { + super(message); + } + + @NonNull + static JsonSchemaMismatchException objectHasMissingAndExtraneousFields( + Set extraneous, Set missing) { + StringBuilder msg = + new StringBuilder("JSON object does not match UDT definition: found ") + .append(extraneous.size()) + .append(" extraneous field"); + if (extraneous.size() > 1) { + msg.append('s'); + } + msg.append(": '") + .append(String.join("', '", extraneous)) + .append("' and ") + .append(missing.size()) + .append(" missing field"); + if (missing.size() > 1) { + msg.append('s'); + } + msg.append(": '") + .append(String.join("', '", missing)) + .append("' (set schema.allowExtraFields to true to allow ") + .append("JSON objects to contain fields not present in the UDT definition and ") + .append("set schema.allowMissingFields to true to allow ") + .append("JSON objects to lack of fields present in the UDT definition)."); + return new JsonSchemaMismatchException(msg.toString()); + } + + @NonNull + static JsonSchemaMismatchException objectHasExtraneousFields(Set extraneous) { + StringBuilder msg = + new StringBuilder("JSON object does not match UDT definition: found ") + .append(extraneous.size()) + .append(" extraneous field"); + if (extraneous.size() > 1) { + msg.append('s'); + } + msg.append(": '") + .append(String.join("', '", extraneous)) + .append("' (set schema.allowExtraFields to true to allow ") + .append("JSON objects to contain fields not present in the UDT definition)."); + return new JsonSchemaMismatchException(msg.toString()); + } + + @NonNull + static JsonSchemaMismatchException objectHasMissingFields(Set missing) { + StringBuilder msg = + new StringBuilder("JSON object does not match UDT definition: found ") + .append(missing.size()) + .append(" missing field"); + if (missing.size() > 1) { + msg.append('s'); + } + msg.append(": '") + .append(String.join("', '", missing)) + .append("' (set schema.allowMissingFields to true to allow ") + .append("JSON objects to lack of fields present in the UDT definition)."); + return new JsonSchemaMismatchException(msg.toString()); + } + + @NonNull + static JsonSchemaMismatchException arraySizeGreaterThanUDTSize(int udtSize, int nodeSize) { + return new JsonSchemaMismatchException( + String.format( + "JSON array does not match UDT definition: expecting %d elements, got %d " + + "(set schema.allowExtraFields to true to allow " + + "JSON arrays to contain more elements than the UDT definition).", + udtSize, nodeSize)); + } + + @NonNull + static JsonSchemaMismatchException arraySizeLesserThanUDTSize(int udtSize, int nodeSize) { + return new JsonSchemaMismatchException( + String.format( + "JSON array does not match UDT definition: expecting %d elements, got %d " + + "(set schema.allowMissingFields to true to allow " + + "JSON arrays to contain fewer elements than the UDT definition).", + udtSize, nodeSize)); + } + + @NonNull + static JsonSchemaMismatchException arraySizeLesserThanTupleSize(int tupleSize, int nodeSize) { + return new JsonSchemaMismatchException( + String.format( + "JSON array does not match tuple definition: expecting %d elements, got %d " + + "(set schema.allowMissingFields to true to allow " + + "JSON arrays to contain fewer elements than the tuple definition).", + tupleSize, nodeSize)); + } + + @NonNull + static JsonSchemaMismatchException arraySizeGreaterThanTupleSize(int tupleSize, int nodeSize) { + return new JsonSchemaMismatchException( + String.format( + "JSON array does not match tuple definition: expecting %d elements, got %d " + + "(set schema.allowExtraFields to true to allow " + + "JSON arrays to contain more elements than the tuple definition).", + tupleSize, nodeSize)); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToDateRangeCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToDateRangeCodec.java new file mode 100644 index 0000000..352eda9 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToDateRangeCodec.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; + +import com.datastax.dse.driver.api.core.data.time.DateRange; +import com.datastax.dse.driver.internal.core.type.codec.time.DateRangeCodec; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.text.json.JsonNodeConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.List; + +public class JsonNodeToDateRangeCodec extends JsonNodeConvertingCodec { + + public JsonNodeToDateRangeCodec(List nullStrings) { + super(new DateRangeCodec(), nullStrings); + } + + @Override + public DateRange externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + return CodecUtils.parseDateRange(node.asText()); + } + + @Override + public JsonNode internalToExternal(DateRange value) { + return value == null ? null : JSON_NODE_FACTORY.textNode(value.toString()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToGeometryCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToGeometryCodec.java new file mode 100644 index 0000000..0506338 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToGeometryCodec.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import com.datastax.dse.driver.api.core.data.geometry.Geometry; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.text.json.JsonNodeConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.util.List; + +public abstract class JsonNodeToGeometryCodec + extends JsonNodeConvertingCodec { + + protected final ObjectMapper objectMapper; + protected final GeoFormat geoFormat; + + protected JsonNodeToGeometryCodec( + TypeCodec targetCodec, + ObjectMapper objectMapper, + GeoFormat geoFormat, + List nullStrings) { + super(targetCodec, nullStrings); + this.objectMapper = objectMapper; + this.geoFormat = geoFormat; + } + + @Override + public T externalToInternal(JsonNode node) { + if (isNullOrEmpty(node)) { + return null; + } + try { + // We accept: + // 1) String nodes containing WKT literals + // 2) String nodes containing Geo JSON documents + // 3) String nodes containing WKB encoded strings + // 4) Json object nodes compliant with Geo JSON syntax + // 5) Binary nodes containing WKB strings + String s; + if (node.isObject()) { + s = objectMapper.writeValueAsString(node); + return parseGeometry(s); + } else if (node.isBinary()) { + byte[] bytes = node.binaryValue(); + return parseGeometry(bytes); + } else { + s = node.asText(); + return parseGeometry(s); + } + } catch (IOException e) { + throw new IllegalArgumentException("Cannot deserialize node " + node, e); + } + } + + protected abstract T parseGeometry(@NonNull String s); + + protected abstract T parseGeometry(@NonNull byte[] b); + + @Override + public JsonNode internalToExternal(T value) { + if (value == null) { + return null; + } + try { + String s = geoFormat.format(value); + if (geoFormat == JsonGeoFormat.INSTANCE) { + // Return a JSON object for GeoJson format instead of a text node + return objectMapper.readTree(s); + } else { + // Return a text node for Well-known text and Well-known binary formats. + return objectMapper.getNodeFactory().textNode(s); + } + } catch (IOException e) { + throw new IllegalArgumentException("Cannot serialize value " + value, e); + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToLineStringCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToLineStringCodec.java new file mode 100644 index 0000000..c060230 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToLineStringCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import com.datastax.dse.driver.api.core.data.geometry.LineString; +import com.datastax.dse.driver.api.core.type.codec.DseTypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.List; + +public class JsonNodeToLineStringCodec extends JsonNodeToGeometryCodec { + + public JsonNodeToLineStringCodec( + ObjectMapper objectMapper, GeoFormat geoFormat, List nullStrings) { + super(DseTypeCodecs.LINE_STRING, objectMapper, geoFormat, nullStrings); + } + + @Override + protected LineString parseGeometry(@NonNull String s) { + return CodecUtils.parseLineString(s); + } + + @Override + protected LineString parseGeometry(@NonNull byte[] b) { + return LineString.fromWellKnownBinary(ByteBuffer.wrap(b)); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPointCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPointCodec.java new file mode 100644 index 0000000..1ac02ba --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPointCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import com.datastax.dse.driver.api.core.data.geometry.Point; +import com.datastax.dse.driver.api.core.type.codec.DseTypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.List; + +public class JsonNodeToPointCodec extends JsonNodeToGeometryCodec { + + public JsonNodeToPointCodec( + ObjectMapper objectMapper, GeoFormat geoFormat, List nullStrings) { + super(DseTypeCodecs.POINT, objectMapper, geoFormat, nullStrings); + } + + @Override + protected Point parseGeometry(@NonNull String s) { + return CodecUtils.parsePoint(s); + } + + @Override + protected Point parseGeometry(@NonNull byte[] b) { + return Point.fromWellKnownBinary(ByteBuffer.wrap(b)); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPolygonCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPolygonCodec.java new file mode 100644 index 0000000..b4fa2b3 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPolygonCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import com.datastax.dse.driver.api.core.data.geometry.Polygon; +import com.datastax.dse.driver.api.core.type.codec.DseTypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.List; + +public class JsonNodeToPolygonCodec extends JsonNodeToGeometryCodec { + + public JsonNodeToPolygonCodec( + ObjectMapper objectMapper, GeoFormat geoFormat, List nullStrings) { + super(DseTypeCodecs.POLYGON, objectMapper, geoFormat, nullStrings); + } + + @Override + protected Polygon parseGeometry(@NonNull String s) { + return CodecUtils.parsePolygon(s); + } + + @Override + protected Polygon parseGeometry(@NonNull byte[] b) { + return Polygon.fromWellKnownBinary(ByteBuffer.wrap(b)); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringConvertingCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringConvertingCodec.java new file mode 100644 index 0000000..69b0f67 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringConvertingCodec.java @@ -0,0 +1,74 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import java.util.List; + +public abstract class StringConvertingCodec extends ConvertingCodec { + + private final List nullStrings; + + protected StringConvertingCodec(TypeCodec targetCodec, List nullStrings) { + super(targetCodec, String.class); + this.nullStrings = nullStrings; + } + + /** + * Whether the input is null. + * + *

This method should be used to inspect external inputs that are meant to be converted to + * textual CQL types only (text, varchar and ascii). + * + *

It always considers the empty string as NOT equivalent to NULL, unless the user clearly + * specifies that the empty string is to be considered as NULL, through the + * codec.nullStrings setting. + * + *

Do NOT use this method for non-textual CQL types; use {@link #isNullOrEmpty(String)} + * instead. + */ + protected boolean isNull(String s) { + return s == null || nullStrings.contains(s); + } + + /** + * Whether the input is null or empty. + * + *

This method should be used to inspect external inputs that are meant to be converted to + * non-textual CQL types only. + * + *

It always considers the empty string as equivalent to NULL, which is in compliance with the + * documentation of codec.nullStrings: "Note that, regardless of this setting, DSBulk + * will always convert empty strings to `null` if the target CQL type is not textual (i.e. not + * text, varchar or ascii)." + * + *

Do NOT use this method for textual CQL types; use {@link #isNull(String)} instead. + */ + protected boolean isNullOrEmpty(String s) { + return isNull(s) || s.isEmpty(); + } + + /** + * The string to use when formatting internal inputs. + * + *

According to the documentation of codec.nullStrings, we must use the first + * string specified there, or simply return null, if that setting is empty. + */ + protected String nullString() { + return nullStrings.isEmpty() ? null : nullStrings.get(0); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringConvertingCodecProvider.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringConvertingCodecProvider.java new file mode 100644 index 0000000..3c89984 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringConvertingCodecProvider.java @@ -0,0 +1,359 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.codecs.api.CommonConversionContext.BINARY_FORMAT; +import static com.datastax.oss.dsbulk.codecs.api.CommonConversionContext.GEO_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.BOOLEAN_INPUT_WORDS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.BOOLEAN_NUMBERS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.BOOLEAN_OUTPUT_WORDS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.EPOCH; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.LOCAL_DATE_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.LOCAL_TIME_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.NULL_STRINGS; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.NUMBER_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.OBJECT_MAPPER; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.OVERFLOW_STRATEGY; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.ROUNDING_MODE; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIMESTAMP_FORMAT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIME_UNIT; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIME_UUID_GENERATOR; +import static com.datastax.oss.dsbulk.codecs.text.TextConversionContext.TIME_ZONE; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.ASCII; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BIGINT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BLOB; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BOOLEAN; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.COUNTER; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.CUSTOM; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DATE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DECIMAL; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DOUBLE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DURATION; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.FLOAT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.INET; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.INT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.LIST; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.MAP; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.SET; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.SMALLINT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIME; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIMESTAMP; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIMEUUID; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TINYINT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TUPLE; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.UDT; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.UUID; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.VARCHAR; +import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.VARINT; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.CqlVectorType; +import com.datastax.oss.driver.api.core.type.CustomType; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecProvider; +import com.datastax.oss.dsbulk.codecs.text.string.dse.StringToDateRangeCodec; +import com.datastax.oss.dsbulk.codecs.text.string.dse.StringToLineStringCodec; +import com.datastax.oss.dsbulk.codecs.text.string.dse.StringToPointCodec; +import com.datastax.oss.dsbulk.codecs.text.string.dse.StringToPolygonCodec; +import com.fasterxml.jackson.databind.JsonNode; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A specialized {@link DefaultCodecRegistry} that is capable of producing {@link ConvertingCodec}s. + */ +public class StringConvertingCodecProvider implements ConvertingCodecProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(StringConvertingCodecProvider.class); + + private static final String LINE_STRING_CLASS_NAME = + "org.apache.cassandra.db.marshal.LineStringType"; + private static final String POINT_CLASS_NAME = "org.apache.cassandra.db.marshal.PointType"; + private static final String POLYGON_CLASS_NAME = "org.apache.cassandra.db.marshal.PolygonType"; + private static final String DATE_RANGE_CLASS_NAME = + "org.apache.cassandra.db.marshal.DateRangeType"; + + @NonNull + @Override + public Optional> maybeProvide( + @NonNull DataType cqlType, + @NonNull GenericType externalJavaType, + @NonNull ConvertingCodecFactory codecFactory, + boolean rootCodec) { + if (!externalJavaType.equals(GenericType.STRING)) { + return Optional.empty(); + } + ConvertingCodec codec = + createStringConvertingCodec(cqlType, codecFactory, rootCodec); + return Optional.ofNullable(codec); + } + + @Nullable + private ConvertingCodec createStringConvertingCodec( + @NonNull DataType cqlType, @NonNull ConvertingCodecFactory codecFactory, boolean rootCodec) { + ConversionContext context = codecFactory.getContext(); + // DAT-297: Don't apply null strings for non-root codecs + List nullStrings = rootCodec ? context.getAttribute(NULL_STRINGS) : ImmutableList.of(); + int cqlTypeCode = cqlType.getProtocolCode(); + switch (cqlTypeCode) { + case ASCII: + case VARCHAR: + TypeCodec typeCodec = codecFactory.getCodecRegistry().codecFor(cqlType); + return new StringToStringCodec(typeCodec, nullStrings); + case BOOLEAN: + return new StringToBooleanCodec( + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_OUTPUT_WORDS), + nullStrings); + case TINYINT: + return new StringToByteCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case SMALLINT: + return new StringToShortCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case INT: + return new StringToIntegerCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case BIGINT: + return new StringToLongCodec( + TypeCodecs.BIGINT, + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case COUNTER: + return new StringToLongCodec( + TypeCodecs.COUNTER, + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case FLOAT: + return new StringToFloatCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case DOUBLE: + return new StringToDoubleCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case VARINT: + return new StringToBigIntegerCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case DECIMAL: + return new StringToBigDecimalCodec( + context.getAttribute(NUMBER_FORMAT), + context.getAttribute(OVERFLOW_STRATEGY), + context.getAttribute(ROUNDING_MODE), + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(TIME_UNIT), + context.getAttribute(EPOCH), + context.getAttribute(BOOLEAN_INPUT_WORDS), + context.getAttribute(BOOLEAN_NUMBERS), + nullStrings); + case DATE: + return new StringToLocalDateCodec( + context.getAttribute(LOCAL_DATE_FORMAT), context.getAttribute(TIME_ZONE), nullStrings); + case TIME: + return new StringToLocalTimeCodec( + context.getAttribute(LOCAL_TIME_FORMAT), context.getAttribute(TIME_ZONE), nullStrings); + case TIMESTAMP: + return new StringToInstantCodec( + context.getAttribute(TIMESTAMP_FORMAT), + context.getAttribute(TIME_ZONE), + context.getAttribute(EPOCH), + nullStrings); + case INET: + return new StringToInetAddressCodec(nullStrings); + case UUID: + { + ConvertingCodec instantCodec = + codecFactory.createConvertingCodec(DataTypes.TIMESTAMP, GenericType.STRING, false); + return new StringToUUIDCodec( + TypeCodecs.UUID, + instantCodec, + context.getAttribute(TIME_UUID_GENERATOR), + nullStrings); + } + case TIMEUUID: + { + ConvertingCodec instantCodec = + codecFactory.createConvertingCodec(DataTypes.TIMESTAMP, GenericType.STRING, false); + return new StringToUUIDCodec( + TypeCodecs.TIMEUUID, + instantCodec, + context.getAttribute(TIME_UUID_GENERATOR), + nullStrings); + } + case BLOB: + return new StringToBlobCodec(nullStrings, context.getAttribute(BINARY_FORMAT)); + case DURATION: + return new StringToDurationCodec(nullStrings); + case LIST: + { + ConvertingCodec> jsonCodec = + codecFactory.createConvertingCodec(cqlType, JSON_NODE_TYPE, false); + return new StringToListCodec<>( + jsonCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case SET: + { + ConvertingCodec> jsonCodec = + codecFactory.createConvertingCodec(cqlType, JSON_NODE_TYPE, false); + return new StringToSetCodec<>( + jsonCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case MAP: + { + ConvertingCodec> jsonCodec = + codecFactory.createConvertingCodec(cqlType, JSON_NODE_TYPE, false); + return new StringToMapCodec<>( + jsonCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case TUPLE: + { + ConvertingCodec jsonCodec = + codecFactory.createConvertingCodec(cqlType, JSON_NODE_TYPE, false); + return new StringToTupleCodec( + jsonCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case UDT: + { + ConvertingCodec jsonCodec = + codecFactory.createConvertingCodec(cqlType, JSON_NODE_TYPE, false); + return new StringToUDTCodec(jsonCodec, context.getAttribute(OBJECT_MAPPER), nullStrings); + } + case CUSTOM: + { + CustomType customType = (CustomType) cqlType; + switch (customType.getClassName()) { + case POINT_CLASS_NAME: + return new StringToPointCodec(context.getAttribute(GEO_FORMAT), nullStrings); + case LINE_STRING_CLASS_NAME: + return new StringToLineStringCodec(context.getAttribute(GEO_FORMAT), nullStrings); + case POLYGON_CLASS_NAME: + return new StringToPolygonCodec(context.getAttribute(GEO_FORMAT), nullStrings); + case DATE_RANGE_CLASS_NAME: + return new StringToDateRangeCodec(nullStrings); + case CqlVectorType.CQLVECTOR_CLASS_NAME: + CqlVectorType cqlVectorType = (CqlVectorType) cqlType; + ConvertingCodec subtypeCodec = + codecFactory.createConvertingCodec( + cqlVectorType.getSubtype(), GenericType.STRING, false); + return new StringToVectorCodec( + new CqlVectorCodec(cqlVectorType, subtypeCodec), nullStrings); + } + } + // fall through + default: + try { + TypeCodec innerCodec = codecFactory.getCodecRegistry().codecFor(cqlType); + LOGGER.warn( + String.format( + "CQL type %s is not officially supported by this version of DSBulk; " + + "string literals will be parsed and formatted using registered codec %s", + cqlType, innerCodec.getClass().getSimpleName())); + return new StringToUnknownTypeCodec<>(innerCodec, nullStrings); + } catch (CodecNotFoundException ignored) { + } + return null; + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigDecimalCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigDecimalCodec.java new file mode 100644 index 0000000..620e497 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigDecimalCodec.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToBigDecimalCodec extends StringToNumberCodec { + + public StringToBigDecimalCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.DECIMAL, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers, + nullStrings); + } + + @Override + public BigDecimal externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return CodecUtils.toBigDecimal(number); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigIntegerCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigIntegerCodec.java new file mode 100644 index 0000000..ee328b4 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigIntegerCodec.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToBigIntegerCodec extends StringToNumberCodec { + + public StringToBigIntegerCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.VARINT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::toBigInteger).collect(toList()), + nullStrings); + } + + @Override + public BigInteger externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, BigInteger.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBlobCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBlobCodec.java new file mode 100644 index 0000000..50bc21a --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBlobCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.binary.BinaryFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.nio.ByteBuffer; +import java.util.List; + +public class StringToBlobCodec extends StringConvertingCodec { + + private final BinaryFormat binaryFormat; + + public StringToBlobCodec(List nullStrings, BinaryFormat binaryFormat) { + super(TypeCodecs.BLOB, nullStrings); + this.binaryFormat = binaryFormat; + } + + @Override + public ByteBuffer externalToInternal(String s) { + // DAT-573: consider empty strings as empty byte arrays + if (isNull(s)) { + return null; + } + return CodecUtils.parseByteBuffer(s); + } + + @Override + public String internalToExternal(ByteBuffer value) { + if (value == null) { + return nullString(); + } + return binaryFormat.format(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBooleanCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBooleanCodec.java new file mode 100644 index 0000000..198b2e7 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBooleanCodec.java @@ -0,0 +1,57 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import java.util.List; +import java.util.Map; + +public class StringToBooleanCodec extends StringConvertingCodec { + + private final Map inputs; + private final Map outputs; + + public StringToBooleanCodec( + Map inputs, Map outputs, List nullStrings) { + super(TypeCodecs.BOOLEAN, nullStrings); + this.inputs = inputs; + this.outputs = outputs; + } + + @Override + public Boolean externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + Boolean b = inputs.get(s.toLowerCase()); + if (b == null) { + throw new IllegalArgumentException("Invalid boolean value: " + s); + } + return b; + } + + @Override + public String internalToExternal(Boolean value) { + if (value == null) { + return nullString(); + } + String s = outputs.get(value); + if (s == null) { + return Boolean.toString(value); + } + return s; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToByteCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToByteCodec.java new file mode 100644 index 0000000..e123adf --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToByteCodec.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToByteCodec extends StringToNumberCodec { + + public StringToByteCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.TINYINT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::byteValueExact).collect(toList()), + nullStrings); + } + + @Override + public Byte externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, Byte.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToCollectionCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToCollectionCodec.java new file mode 100644 index 0000000..bf11c50 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToCollectionCodec.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.datastax.oss.dsbulk.codecs.text.utils.StringUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +public abstract class StringToCollectionCodec> + extends StringConvertingCodec { + + private final ConvertingCodec jsonCodec; + private final ObjectMapper objectMapper; + + StringToCollectionCodec( + ConvertingCodec jsonCodec, ObjectMapper objectMapper, List nullStrings) { + super(jsonCodec.getInternalCodec(), nullStrings); + this.jsonCodec = jsonCodec; + this.objectMapper = objectMapper; + } + + @Override + public C externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + try { + JsonNode node = objectMapper.readTree(StringUtils.ensureBrackets(s)); + return jsonCodec.externalToInternal(node); + } catch (IOException e) { + throw new IllegalArgumentException(String.format("Could not parse '%s' as Json", s), e); + } + } + + @Override + public String internalToExternal(C collection) { + if (collection == null) { + return nullString(); + } + try { + JsonNode node = jsonCodec.internalToExternal(collection); + return objectMapper.writeValueAsString(node); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException( + String.format("Could not format '%s' to Json", collection), e); + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDoubleCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDoubleCodec.java new file mode 100644 index 0000000..c8e5254 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDoubleCodec.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToDoubleCodec extends StringToNumberCodec { + + public StringToDoubleCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.DOUBLE, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::doubleValue).collect(toList()), + nullStrings); + } + + @Override + public Double externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, Double.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDurationCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDurationCodec.java new file mode 100644 index 0000000..411c2e1 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDurationCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import java.util.List; + +public class StringToDurationCodec extends StringConvertingCodec { + + public StringToDurationCodec(List nullStrings) { + super(TypeCodecs.DURATION, nullStrings); + } + + @Override + public CqlDuration externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CqlDuration.from(s); + } + + @Override + public String internalToExternal(CqlDuration value) { + if (value == null) { + return nullString(); + } + return value.toString(); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToFloatCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToFloatCodec.java new file mode 100644 index 0000000..ad46e42 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToFloatCodec.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToFloatCodec extends StringToNumberCodec { + + public StringToFloatCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.FLOAT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::floatValue).collect(toList()), + nullStrings); + } + + @Override + public Float externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, Float.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInetAddressCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInetAddressCodec.java new file mode 100644 index 0000000..31e2655 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInetAddressCodec.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import java.net.InetAddress; +import java.util.List; + +public class StringToInetAddressCodec extends StringConvertingCodec { + + public StringToInetAddressCodec(List nullStrings) { + super(TypeCodecs.INET, nullStrings); + } + + @Override + public InetAddress externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + if (s.isEmpty()) { + throw new IllegalArgumentException("Cannot create inet address from empty string"); + } + try { + return InetAddress.getByName(s); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot parse inet address: " + s); + } + } + + @Override + public String internalToExternal(InetAddress value) { + if (value == null) { + return nullString(); + } + return value.getHostAddress(); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInstantCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInstantCodec.java new file mode 100644 index 0000000..6123c4d --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInstantCodec.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public class StringToInstantCodec extends StringToTemporalCodec { + + private final ZoneId timeZone; + private final ZonedDateTime epoch; + + public StringToInstantCodec( + TemporalFormat temporalFormat, + ZoneId timeZone, + ZonedDateTime epoch, + List nullStrings) { + super(TypeCodecs.TIMESTAMP, temporalFormat, nullStrings); + this.timeZone = timeZone; + this.epoch = epoch; + } + + @Override + public Instant externalToInternal(String s) { + TemporalAccessor temporal = parseTemporalAccessor(s); + if (temporal == null) { + return null; + } + return CodecUtils.toInstant(temporal, timeZone, epoch.toLocalDate()); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToIntegerCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToIntegerCodec.java new file mode 100644 index 0000000..151b875 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToIntegerCodec.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToIntegerCodec extends StringToNumberCodec { + + public StringToIntegerCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.INT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::intValueExact).collect(toList()), + nullStrings); + } + + @Override + public Integer externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, Integer.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToListCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToListCodec.java new file mode 100644 index 0000000..03c7e4c --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToListCodec.java @@ -0,0 +1,31 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; + +public class StringToListCodec extends StringToCollectionCodec> { + + public StringToListCodec( + ConvertingCodec> jsonCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(jsonCodec, objectMapper, nullStrings); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalDateCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalDateCodec.java new file mode 100644 index 0000000..685c5d4 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalDateCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public class StringToLocalDateCodec extends StringToTemporalCodec { + + private final ZoneId timeZone; + + public StringToLocalDateCodec(TemporalFormat parser, ZoneId timeZone, List nullStrings) { + super(TypeCodecs.DATE, parser, nullStrings); + this.timeZone = timeZone; + } + + @Override + public LocalDate externalToInternal(String s) { + TemporalAccessor temporal = parseTemporalAccessor(s); + if (temporal == null) { + return null; + } + return CodecUtils.toLocalDate(temporal, timeZone); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalTimeCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalTimeCodec.java new file mode 100644 index 0000000..d6353cc --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalTimeCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public class StringToLocalTimeCodec extends StringToTemporalCodec { + + private final ZoneId timeZone; + + public StringToLocalTimeCodec(TemporalFormat parser, ZoneId timeZone, List nullStrings) { + super(TypeCodecs.TIME, parser, nullStrings); + this.timeZone = timeZone; + } + + @Override + public LocalTime externalToInternal(String s) { + TemporalAccessor temporal = parseTemporalAccessor(s); + if (temporal == null) { + return null; + } + return CodecUtils.toLocalTime(temporal, timeZone); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLongCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLongCodec.java new file mode 100644 index 0000000..98bd5d8 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLongCodec.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.PrimitiveLongCodec; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToLongCodec extends StringToNumberCodec { + + public StringToLongCodec( + PrimitiveLongCodec targetCodec, + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + targetCodec, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::longValueExact).collect(toList()), + nullStrings); + } + + @Override + public Long externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, Long.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToMapCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToMapCodec.java new file mode 100644 index 0000000..1efce39 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToMapCodec.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.datastax.oss.dsbulk.codecs.text.utils.StringUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class StringToMapCodec extends StringConvertingCodec> { + + private final ConvertingCodec> jsonCodec; + private final ObjectMapper objectMapper; + + public StringToMapCodec( + ConvertingCodec> jsonCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(jsonCodec.getInternalCodec(), nullStrings); + this.jsonCodec = jsonCodec; + this.objectMapper = objectMapper; + } + + @Override + public Map externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + try { + JsonNode node = objectMapper.readTree(StringUtils.ensureBraces(s)); + return jsonCodec.externalToInternal(node); + } catch (IOException e) { + throw new IllegalArgumentException(String.format("Could not parse '%s' as Json", s), e); + } + } + + @Override + public String internalToExternal(Map map) { + if (map == null) { + return nullString(); + } + try { + JsonNode node = jsonCodec.internalToExternal(map); + return objectMapper.writeValueAsString(node); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(String.format("Could not format '%s' to Json", map), e); + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToNumberCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToNumberCodec.java new file mode 100644 index 0000000..f3487aa --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToNumberCodec.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public abstract class StringToNumberCodec extends StringConvertingCodec { + + private final FastThreadLocal numberFormat; + private final OverflowStrategy overflowStrategy; + private final RoundingMode roundingMode; + private final TemporalFormat temporalFormat; + private final ZoneId timeZone; + private final TimeUnit timeUnit; + private final ZonedDateTime epoch; + private final Map booleanStrings; + private final List booleanNumbers; + + StringToNumberCodec( + TypeCodec targetCodec, + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super(targetCodec, nullStrings); + this.numberFormat = numberFormat; + this.overflowStrategy = overflowStrategy; + this.roundingMode = roundingMode; + this.temporalFormat = temporalFormat; + this.timeZone = timeZone; + this.timeUnit = timeUnit; + this.epoch = epoch; + this.booleanStrings = booleanStrings; + this.booleanNumbers = booleanNumbers; + } + + @Override + public String internalToExternal(N value) { + if (value == null) { + return nullString(); + } + return CodecUtils.formatNumber(value, numberFormat.get()); + } + + Number parseNumber(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CodecUtils.parseNumber( + s, + numberFormat.get(), + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers); + } + + N narrowNumber(Number number, Class targetClass) { + return CodecUtils.narrowNumber(number, targetClass, overflowStrategy, roundingMode); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToSetCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToSetCodec.java new file mode 100644 index 0000000..568f5e4 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToSetCodec.java @@ -0,0 +1,32 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Set; + +public class StringToSetCodec extends StringToCollectionCodec> { + + public StringToSetCodec( + ConvertingCodec> jsonCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(jsonCodec, objectMapper, nullStrings); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToShortCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToShortCodec.java new file mode 100644 index 0000000..a063771 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToShortCodec.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static java.util.stream.Collectors.toList; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.OverflowStrategy; +import io.netty.util.concurrent.FastThreadLocal; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class StringToShortCodec extends StringToNumberCodec { + + public StringToShortCodec( + FastThreadLocal numberFormat, + OverflowStrategy overflowStrategy, + RoundingMode roundingMode, + TemporalFormat temporalFormat, + ZoneId timeZone, + TimeUnit timeUnit, + ZonedDateTime epoch, + Map booleanStrings, + List booleanNumbers, + List nullStrings) { + super( + TypeCodecs.SMALLINT, + numberFormat, + overflowStrategy, + roundingMode, + temporalFormat, + timeZone, + timeUnit, + epoch, + booleanStrings, + booleanNumbers.stream().map(BigDecimal::shortValueExact).collect(toList()), + nullStrings); + } + + @Override + public Short externalToInternal(String s) { + Number number = parseNumber(s); + if (number == null) { + return null; + } + return narrowNumber(number, Short.class); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToStringCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToStringCodec.java new file mode 100644 index 0000000..ec7b164 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToStringCodec.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import java.util.List; + +public class StringToStringCodec extends StringConvertingCodec { + + public StringToStringCodec(TypeCodec innerCodec, List nullStrings) { + super(innerCodec, nullStrings); + } + + @Override + public String externalToInternal(String s) { + // DAT-297: do not convert empty strings to null so do not use isNullOrEmpty() here + if (isNull(s)) { + return null; + } + return s; + } + + @Override + public String internalToExternal(String value) { + if (value == null) { + return nullString(); + } + return value; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTemporalCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTemporalCodec.java new file mode 100644 index 0000000..25841fe --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTemporalCodec.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +public abstract class StringToTemporalCodec + extends StringConvertingCodec { + + final TemporalFormat temporalFormat; + + StringToTemporalCodec( + TypeCodec targetCodec, TemporalFormat temporalFormat, List nullStrings) { + super(targetCodec, nullStrings); + this.temporalFormat = temporalFormat; + } + + @Override + public String internalToExternal(T value) { + if (value == null) { + return nullString(); + } + return temporalFormat.format(value); + } + + TemporalAccessor parseTemporalAccessor(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return temporalFormat.parse(s); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTupleCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTupleCodec.java new file mode 100644 index 0000000..7bf0a9c --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTupleCodec.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; + +public class StringToTupleCodec extends StringConvertingCodec { + + private final ConvertingCodec jsonCodec; + private final ObjectMapper objectMapper; + + public StringToTupleCodec( + ConvertingCodec jsonCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(jsonCodec.getInternalCodec(), nullStrings); + this.jsonCodec = jsonCodec; + this.objectMapper = objectMapper; + } + + @Override + public TupleValue externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + try { + JsonNode node = objectMapper.readTree(s); + return jsonCodec.externalToInternal(node); + } catch (IOException e) { + throw new IllegalArgumentException(String.format("Could not parse '%s' as Json", s), e); + } + } + + @Override + public String internalToExternal(TupleValue tuple) { + if (tuple == null) { + return nullString(); + } + try { + JsonNode node = jsonCodec.internalToExternal(tuple); + return objectMapper.writeValueAsString(node); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(String.format("Could not format '%s' to Json", tuple), e); + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUDTCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUDTCodec.java new file mode 100644 index 0000000..f94c148 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUDTCodec.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; + +public class StringToUDTCodec extends StringConvertingCodec { + + private final ConvertingCodec jsonCodec; + private final ObjectMapper objectMapper; + + public StringToUDTCodec( + ConvertingCodec jsonCodec, + ObjectMapper objectMapper, + List nullStrings) { + super(jsonCodec.getInternalCodec(), nullStrings); + this.jsonCodec = jsonCodec; + this.objectMapper = objectMapper; + } + + @Override + public UdtValue externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + try { + JsonNode node = objectMapper.readTree(s); + return jsonCodec.externalToInternal(node); + } catch (IOException e) { + throw new IllegalArgumentException(String.format("Could not parse '%s' as Json", s), e); + } + } + + @Override + public String internalToExternal(UdtValue udt) { + if (udt == null) { + return nullString(); + } + try { + JsonNode node = jsonCodec.internalToExternal(udt); + return objectMapper.writeValueAsString(node); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(String.format("Could not format '%s' to Json", udt), e); + } + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUUIDCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUUIDCodec.java new file mode 100644 index 0000000..9898830 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUUIDCodec.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodec; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.TimeUUIDGenerator; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public class StringToUUIDCodec extends StringConvertingCodec { + + private final ConvertingCodec instantCodec; + private final TimeUUIDGenerator generator; + + public StringToUUIDCodec( + TypeCodec targetCodec, + ConvertingCodec instantCodec, + TimeUUIDGenerator generator, + List nullStrings) { + super(targetCodec, nullStrings); + this.instantCodec = instantCodec; + this.generator = generator; + } + + @Override + public UUID externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CodecUtils.parseUUID(s, instantCodec, generator); + } + + @Override + public String internalToExternal(UUID value) { + if (value == null) { + return nullString(); + } + return value.toString(); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUnknownTypeCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUnknownTypeCodec.java new file mode 100644 index 0000000..d9f3cfb --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUnknownTypeCodec.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import java.util.List; + +public class StringToUnknownTypeCodec extends StringConvertingCodec { + + public StringToUnknownTypeCodec(TypeCodec targetCodec, List nullStrings) { + super(targetCodec, nullStrings); + } + + @Override + public T externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return getInternalCodec().parse(s); + } + + @Override + public String internalToExternal(T o) { + if (o == null) { + return nullString(); + } + String s = getInternalCodec().format(o); + // most codecs usually format null/empty values using the CQL keyword "NULL" + if (s.equalsIgnoreCase("NULL")) { + return nullString(); + } + return s; + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToVectorCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToVectorCodec.java new file mode 100644 index 0000000..69334a4 --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/StringToVectorCodec.java @@ -0,0 +1,37 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import com.datastax.oss.driver.api.core.data.CqlVector; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import java.util.List; + +public class StringToVectorCodec extends StringConvertingCodec> { + + public StringToVectorCodec(CqlVectorCodec subcodec, List nullStrings) { + super(subcodec, nullStrings); + } + + @Override + public CqlVector externalToInternal(String s) { + return this.internalCodec.parse(s); + } + + @Override + public String internalToExternal(CqlVector cqlVector) { + return this.internalCodec.format(cqlVector); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToDateRangeCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToDateRangeCodec.java new file mode 100644 index 0000000..1ecbc5d --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToDateRangeCodec.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import com.datastax.dse.driver.api.core.data.time.DateRange; +import com.datastax.dse.driver.internal.core.type.codec.time.DateRangeCodec; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.text.string.StringConvertingCodec; +import java.util.List; + +public class StringToDateRangeCodec extends StringConvertingCodec { + + public StringToDateRangeCodec(List nullStrings) { + super(new DateRangeCodec(), nullStrings); + } + + @Override + public DateRange externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CodecUtils.parseDateRange(s); + } + + @Override + public String internalToExternal(DateRange value) { + if (value == null) { + return nullString(); + } + return value.toString(); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToLineStringCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToLineStringCodec.java new file mode 100644 index 0000000..abece1e --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToLineStringCodec.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import com.datastax.dse.driver.api.core.data.geometry.LineString; +import com.datastax.dse.driver.api.core.type.codec.DseTypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.text.string.StringConvertingCodec; +import java.util.List; + +public class StringToLineStringCodec extends StringConvertingCodec { + + private final GeoFormat geoFormat; + + public StringToLineStringCodec(GeoFormat geoFormat, List nullStrings) { + super(DseTypeCodecs.LINE_STRING, nullStrings); + this.geoFormat = geoFormat; + } + + @Override + public LineString externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CodecUtils.parseLineString(s); + } + + @Override + public String internalToExternal(LineString value) { + return geoFormat.format(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPointCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPointCodec.java new file mode 100644 index 0000000..183be4e --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPointCodec.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import com.datastax.dse.driver.api.core.data.geometry.Point; +import com.datastax.dse.driver.api.core.type.codec.DseTypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.text.string.StringConvertingCodec; +import java.util.List; + +public class StringToPointCodec extends StringConvertingCodec { + + private final GeoFormat geoFormat; + + public StringToPointCodec(GeoFormat geoFormat, List nullStrings) { + super(DseTypeCodecs.POINT, nullStrings); + this.geoFormat = geoFormat; + } + + @Override + public Point externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CodecUtils.parsePoint(s); + } + + @Override + public String internalToExternal(Point value) { + return geoFormat.format(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPolygonCodec.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPolygonCodec.java new file mode 100644 index 0000000..709f37a --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPolygonCodec.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import com.datastax.dse.driver.api.core.data.geometry.Polygon; +import com.datastax.dse.driver.api.core.type.codec.DseTypeCodecs; +import com.datastax.oss.dsbulk.codecs.api.format.geo.GeoFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.text.string.StringConvertingCodec; +import java.util.List; + +public class StringToPolygonCodec extends StringConvertingCodec { + + private final GeoFormat geoFormat; + + public StringToPolygonCodec(GeoFormat geoFormat, List nullStrings) { + super(DseTypeCodecs.POLYGON, nullStrings); + this.geoFormat = geoFormat; + } + + @Override + public Polygon externalToInternal(String s) { + if (isNullOrEmpty(s)) { + return null; + } + return CodecUtils.parsePolygon(s); + } + + @Override + public String internalToExternal(Polygon value) { + return geoFormat.format(value); + } +} diff --git a/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/utils/StringUtils.java b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/utils/StringUtils.java new file mode 100644 index 0000000..3311efa --- /dev/null +++ b/text/src/main/java/com/datastax/oss/dsbulk/codecs/text/utils/StringUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.utils; + +public class StringUtils { + + /** + * If the given string is surrounded by square brackets, return it intact, otherwise trim it and + * surround it with square brackets. + * + * @param value The string to check. + * @return A string surrounded by square brackets. + */ + public static String ensureBrackets(String value) { + value = value.trim(); + if (value.startsWith("[") && value.endsWith("]")) { + return value; + } + return "[" + value + "]"; + } + + /** + * If the given string is surrounded by curly braces, return it intact, otherwise trim it and + * surround it with curly braces. + * + * @param value The string to check. + * @return A string surrounded by curly braces. + */ + public static String ensureBraces(String value) { + value = value.trim(); + if (value.startsWith("{") && value.endsWith("}")) { + return value; + } + return "{" + value + "}"; + } +} diff --git a/text/src/main/resources/META-INF/services/com.datastax.oss.dsbulk.codecs.api.ConvertingCodecProvider b/text/src/main/resources/META-INF/services/com.datastax.oss.dsbulk.codecs.api.ConvertingCodecProvider new file mode 100644 index 0000000..0c69bc9 --- /dev/null +++ b/text/src/main/resources/META-INF/services/com.datastax.oss.dsbulk.codecs.api.ConvertingCodecProvider @@ -0,0 +1,2 @@ +com.datastax.oss.dsbulk.codecs.text.string.StringConvertingCodecProvider +com.datastax.oss.dsbulk.codecs.text.json.JsonNodeConvertingCodecProvider diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigDecimalCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigDecimalCodecTest.java new file mode 100644 index 0000000..6ec93d8 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigDecimalCodecTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.BigDecimal.ONE; +import static java.math.BigDecimal.ZERO; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToBigDecimalCodecTest { + + JsonNodeToBigDecimalCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToBigDecimalCodec) + codecFactory.createConvertingCodec( + DataTypes.DECIMAL, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0)) + .toInternal(ZERO) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0d)) + .toInternal(new BigDecimal("0.0")) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(ONE)) + .toInternal(ONE) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(-1234.56)) + .toInternal(new BigDecimal("-1234.56")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-1,234.56")) + .toInternal(new BigDecimal("-1234.56")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal(new BigDecimal("0")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) + .toInternal(new BigDecimal("946684800000")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal(ONE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal(ZERO) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(ZERO) + .toExternal(JSON_NODE_FACTORY.numberNode(ZERO)) + .convertsFromInternal(new BigDecimal("1234.56")) + .toExternal(JSON_NODE_FACTORY.numberNode(new BigDecimal("1234.56"))) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid decimal")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigIntegerCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigIntegerCodecTest.java new file mode 100644 index 0000000..c2efed7 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBigIntegerCodecTest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import java.math.BigInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToBigIntegerCodecTest { + + private JsonNodeToBigIntegerCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToBigIntegerCodec) + codecFactory.createConvertingCodec( + DataTypes.VARINT, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0)) + .toInternal(BigInteger.ZERO) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0d)) + .toInternal(new BigInteger("0")) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(BigInteger.ONE)) + .toInternal(BigInteger.ONE) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(-1234)) + .toInternal(new BigInteger("-1234")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-1,234")) + .toInternal(new BigInteger("-1234")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal(new BigInteger("0")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) + .toInternal(new BigInteger("946684800000")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal(BigInteger.ONE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal(BigInteger.ZERO) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(BigInteger.ZERO) + .toExternal(JSON_NODE_FACTORY.numberNode(BigInteger.ZERO)) + .convertsFromInternal(new BigInteger("-1234")) + .toExternal(JSON_NODE_FACTORY.numberNode(new BigInteger("-1234"))) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid biginteger")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBlobCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBlobCodecTest.java new file mode 100644 index 0000000..558b141 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBlobCodecTest.java @@ -0,0 +1,112 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.binary.Base64BinaryFormat; +import com.datastax.oss.dsbulk.codecs.api.format.binary.HexBinaryFormat; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.Base64; +import org.junit.jupiter.api.Test; + +class JsonNodeToBlobCodecTest { + + private final byte[] data = {1, 2, 3, 4, 5, 6}; + private final byte[] empty = {}; + + private final ByteBuffer dataBb = ByteBuffer.wrap(data); + private final ByteBuffer emptyBb = ByteBuffer.wrap(empty); + + private final String data64 = Base64.getEncoder().encodeToString(data); + private final String dataHex = Bytes.toHexString(data); + + private final String empty64 = Base64.getEncoder().encodeToString(empty); + private final String emptyHex = Bytes.toHexString(empty); + + private final JsonNodeToBlobCodec codecBase64 = + new JsonNodeToBlobCodec(Base64BinaryFormat.INSTANCE, Lists.newArrayList("NULL")); + + private final JsonNodeToBlobCodec codecHex = + new JsonNodeToBlobCodec(HexBinaryFormat.INSTANCE, Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() { + assertThat(codecBase64) + .convertsFromExternal(JSON_NODE_FACTORY.binaryNode(data)) + .toInternal(dataBb) + .convertsFromExternal(JSON_NODE_FACTORY.binaryNode(empty)) + .toInternal(emptyBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode(data64)) + .toInternal(dataBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode(dataHex)) + .toInternal(dataBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0x")) + .toInternal(emptyBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + // DAT-573: consider empty string as empty byte array + .toInternal(emptyBb) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + assertThat(codecHex) + .convertsFromExternal(JSON_NODE_FACTORY.binaryNode(data)) + .toInternal(dataBb) + .convertsFromExternal(JSON_NODE_FACTORY.binaryNode(empty)) + .toInternal(emptyBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode(data64)) + .toInternal(dataBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode(dataHex)) + .toInternal(dataBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0x")) + .toInternal(emptyBb) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + // DAT-573: consider empty string as empty byte array + .toInternal(emptyBb) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codecBase64) + .convertsFromInternal(dataBb) + .toExternal(JSON_NODE_FACTORY.textNode(data64)) + .convertsFromInternal(emptyBb) + .toExternal(JSON_NODE_FACTORY.textNode(empty64)) + .convertsFromInternal(null) + .toExternal(null); + assertThat(codecHex) + .convertsFromInternal(dataBb) + .toExternal(JSON_NODE_FACTORY.textNode(dataHex)) + .convertsFromInternal(emptyBb) + .toExternal(JSON_NODE_FACTORY.textNode(emptyHex)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codecBase64) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid binary")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBooleanCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBooleanCodecTest.java new file mode 100644 index 0000000..77f0df8 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToBooleanCodecTest.java @@ -0,0 +1,74 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class JsonNodeToBooleanCodecTest { + + private final Map inputs = + ImmutableMap.builder().put("foo", true).put("bar", false).build(); + + private final JsonNodeToBooleanCodec codec = + new JsonNodeToBooleanCodec(inputs, Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.booleanNode(true)) + .toInternal(true) + .convertsFromExternal(JSON_NODE_FACTORY.booleanNode(false)) + .toInternal(false) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FOO")) + .toInternal(true) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("BAR")) + .toInternal(false) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("foo")) + .toInternal(true) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("bar")) + .toInternal(false) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(true) + .toExternal(JSON_NODE_FACTORY.booleanNode(true)) + .convertsFromInternal(false) + .toExternal(JSON_NODE_FACTORY.booleanNode(false)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid boolean")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToByteCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToByteCodecTest.java new file mode 100644 index 0000000..e603548 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToByteCodecTest.java @@ -0,0 +1,96 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToByteCodecTest { + + private JsonNodeToByteCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToByteCodec) + codecFactory.createConvertingCodec( + DataTypes.TINYINT, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode((byte) 0)) + .toInternal((byte) 0) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode((byte) 127)) + .toInternal((byte) 127) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode((byte) -128)) + .toInternal((byte) -128) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0")) + .toInternal((byte) 0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("127")) + .toInternal((byte) 127) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-128")) + .toInternal((byte) -128) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal((byte) 0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal((byte) 1) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal((byte) 0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal((byte) 0) + .toExternal(JSON_NODE_FACTORY.numberNode((byte) 0)) + .convertsFromInternal((byte) 127) + .toExternal(JSON_NODE_FACTORY.numberNode((byte) 127)) + .convertsFromInternal((byte) -128) + .toExternal(JSON_NODE_FACTORY.numberNode((byte) -128)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid byte")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.numberNode(1.2)) + .cannotConvertFromExternal(JSON_NODE_FACTORY.numberNode(128)) + .cannotConvertFromExternal(JSON_NODE_FACTORY.numberNode(-129)) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) // overflow + ; + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDoubleCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDoubleCodecTest.java new file mode 100644 index 0000000..83ebe46 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDoubleCodecTest.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToDoubleCodecTest { + + private JsonNodeToDoubleCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToDoubleCodec) + codecFactory.createConvertingCodec( + DataTypes.DOUBLE, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0)) + .toInternal(0d) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(1234.56d)) + .toInternal(1234.56d) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(1.7976931348623157E308d)) + .toInternal(Double.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(4.9E-324d)) + .toInternal(Double.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0")) + .toInternal(0d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1234.56")) + .toInternal(1234.56d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1,234.56")) + .toInternal(1234.56d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1.7976931348623157E308")) + .toInternal(Double.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("4.9E-324")) + .toInternal(Double.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal(0d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) + .toInternal(946684800000d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal(1d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal(0d) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0d) + .toExternal(JSON_NODE_FACTORY.numberNode(0d)) + .convertsFromInternal(1234.56d) + .toExternal(JSON_NODE_FACTORY.numberNode(1234.56d)) + .convertsFromInternal(0.001d) + .toExternal(JSON_NODE_FACTORY.numberNode(0.001d)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid double")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDurationCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDurationCodecTest.java new file mode 100644 index 0000000..ba60e49 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToDurationCodecTest.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import org.junit.jupiter.api.Test; + +class JsonNodeToDurationCodecTest { + + private final long nanosPerMinute = 60 * 1000L * 1000L * 1000L; + + private final CqlDuration duration = CqlDuration.newInstance(15, 0, 130 * nanosPerMinute); + + private final JsonNodeToDurationCodec codec = + new JsonNodeToDurationCodec(Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1y3mo2h10m")) // standard pattern + .toInternal(duration) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("P1Y3MT2H10M")) // ISO 8601 pattern + .toInternal(duration) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode("P0001-03-00T02:10:00")) // alternative ISO 8601 pattern + .toInternal(duration) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(duration) + .toExternal(JSON_NODE_FACTORY.textNode("1y3mo2h10m")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal( + JSON_NODE_FACTORY.textNode("1Y3M4D")) // The minutes should be after days + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid duration")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToFloatCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToFloatCodecTest.java new file mode 100644 index 0000000..c9f9e39 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToFloatCodecTest.java @@ -0,0 +1,113 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToFloatCodecTest { + + private JsonNodeToFloatCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToFloatCodec) + codecFactory.createConvertingCodec( + DataTypes.FLOAT, JSON_NODE_TYPE, true); + } + + @Test + @SuppressWarnings("FloatingPointLiteralPrecision") + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0f)) + .toInternal(0f) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(1234.56f)) + .toInternal(1234.56f) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(3.4028235E38f)) + .toInternal(Float.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(1.4E-45f)) + .toInternal(Float.MIN_VALUE) + .convertsFromExternal( + JSON_NODE_FACTORY.numberNode(340_282_346_638_528_860_000_000_000_000_000_000_000f)) + .toInternal(Float.MAX_VALUE) + .convertsFromExternal( + JSON_NODE_FACTORY.numberNode(0.0000000000000000000000000000000000000000000014f)) + .toInternal(Float.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0")) + .toInternal(0f) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1234.56")) + .toInternal(1234.56f) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1,234.56")) + .toInternal(1234.56f) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("3.4028235E38")) + .toInternal(Float.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1.4E-45")) + .toInternal(Float.MIN_VALUE) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode("340,282,350,000,000,000,000,000,000,000,000,000,000")) + .toInternal(Float.MAX_VALUE) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode("0.0000000000000000000000000000000000000000000014")) + .toInternal(Float.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal(0f) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal(1f) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal(0f) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + @SuppressWarnings("FloatingPointLiteralPrecision") + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0f) + .toExternal(JSON_NODE_FACTORY.numberNode(0f)) + .convertsFromInternal(1234.56f) + .toExternal(JSON_NODE_FACTORY.numberNode(1234.56f)) + .convertsFromInternal(Float.MAX_VALUE) + .toExternal( + JSON_NODE_FACTORY.numberNode(340_282_350_000_000_000_000_000_000_000_000_000_000f)) + .convertsFromInternal(0.001f) + .toExternal(JSON_NODE_FACTORY.numberNode(0.001f)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid float")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInetAddressCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInetAddressCodecTest.java new file mode 100644 index 0000000..f21a058 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInetAddressCodecTest.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.net.InetAddress; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class JsonNodeToInetAddressCodecTest { + + private final JsonNodeToInetAddressCodec codec = + new JsonNodeToInetAddressCodec(Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1.2.3.4")) + .toInternal(InetAddress.getByName("1.2.3.4")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("127.0.0.1")) + .toInternal(InetAddress.getByName("127.0.0.1")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(codec) + .convertsFromInternal(InetAddress.getByName("1.2.3.4")) + .toExternal(JSON_NODE_FACTORY.textNode("1.2.3.4")) + .convertsFromInternal(InetAddress.getByName("127.0.0.1")) + .toExternal(JSON_NODE_FACTORY.textNode("127.0.0.1")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + @Disabled + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid inet address")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInstantCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInstantCodecTest.java new file mode 100644 index 0000000..77ffafe --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToInstantCodecTest.java @@ -0,0 +1,160 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.util.concurrent.TimeUnit.MINUTES; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.CqlTemporalFormat; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToInstantCodecTest { + + private final Instant millennium = Instant.parse("2000-01-01T00:00:00Z"); + private final Instant minutesAfterMillennium = millennium.plus(Duration.ofMinutes(123456)); + + private JsonNodeToInstantCodec codec1; + private JsonNodeToInstantCodec codec2; + private JsonNodeToInstantCodec codec3; + private JsonNodeToInstantCodec codec4; + + @BeforeEach + void setUpCodec1() { + ConversionContext context1 = new TextConversionContext().setNullStrings("NULL"); + ConversionContext context2 = + new TextConversionContext().setNullStrings("NULL").setTimestampFormat("yyyyMMddHHmmss"); + ConversionContext context3 = + new TextConversionContext() + .setNullStrings("NULL") + .setTimeUnit(MINUTES) + .setEpoch(ZonedDateTime.parse("2000-01-01T00:00:00Z")); + ConversionContext context4 = + new TextConversionContext() + .setNullStrings("NULL") + .setTimeUnit(MINUTES) + .setEpoch(ZonedDateTime.parse("2000-01-01T00:00:00Z")) + .setTimestampFormat("UNITS_SINCE_EPOCH"); + codec1 = + (JsonNodeToInstantCodec) + new ConvertingCodecFactory(context1) + .createConvertingCodec( + DataTypes.TIMESTAMP, JSON_NODE_TYPE, true); + codec2 = + (JsonNodeToInstantCodec) + new ConvertingCodecFactory(context2) + .createConvertingCodec( + DataTypes.TIMESTAMP, JSON_NODE_TYPE, true); + codec3 = + (JsonNodeToInstantCodec) + new ConvertingCodecFactory(context3) + .createConvertingCodec( + DataTypes.TIMESTAMP, JSON_NODE_TYPE, true); + codec4 = + (JsonNodeToInstantCodec) + new ConvertingCodecFactory(context4) + .createConvertingCodec( + DataTypes.TIMESTAMP, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec1) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34")) + .toInternal(Instant.parse("2016-07-24T20:34:00Z")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34:12")) + .toInternal(Instant.parse("2016-07-24T20:34:12Z")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34:12.999")) + .toInternal(Instant.parse("2016-07-24T20:34:12.999Z")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34+01:00")) + .toInternal(Instant.parse("2016-07-24T19:34:00Z")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34:12.999+01:00")) + .toInternal(Instant.parse("2016-07-24T19:34:12.999Z")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + assertThat(codec2) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("20160724203412")) + .toInternal(Instant.parse("2016-07-24T20:34:12Z")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + assertThat(codec3) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode( + CqlTemporalFormat.DEFAULT_INSTANCE.format(minutesAfterMillennium))) + .toInternal(minutesAfterMillennium); + assertThat(codec4) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(123456)) + .toInternal(minutesAfterMillennium) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("123456")) + .toInternal(minutesAfterMillennium); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec1) + .convertsFromInternal(Instant.parse("2016-07-24T20:34:00Z")) + .toExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34:00Z")) + .convertsFromInternal(Instant.parse("2016-07-24T20:34:12Z")) + .toExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34:12Z")) + .convertsFromInternal(Instant.parse("2016-07-24T20:34:12.999Z")) + .toExternal(JSON_NODE_FACTORY.textNode("2016-07-24T20:34:12.999Z")) + .convertsFromInternal(Instant.parse("2016-07-24T19:34:00.000Z")) + .toExternal(JSON_NODE_FACTORY.textNode("2016-07-24T19:34:00Z")) + .convertsFromInternal(Instant.parse("2016-07-24T19:34:12.999Z")) + .toExternal(JSON_NODE_FACTORY.textNode("2016-07-24T19:34:12.999Z")) + .convertsFromInternal(null) + .toExternal(null); + assertThat(codec2) + .convertsFromInternal(Instant.parse("2016-07-24T20:34:12Z")) + .toExternal(JSON_NODE_FACTORY.textNode("20160724203412")) + .convertsFromInternal(null) + .toExternal(null); + // conversion back to numeric timestamps is not possible, values are always formatted with full + // alphanumeric pattern + assertThat(codec3) + .convertsFromInternal(minutesAfterMillennium) + .toExternal( + JSON_NODE_FACTORY.textNode( + CqlTemporalFormat.DEFAULT_INSTANCE.format(minutesAfterMillennium))); + assertThat(codec4) + .convertsFromInternal(minutesAfterMillennium) + .toExternal(JSON_NODE_FACTORY.numberNode(123456L)); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec1) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid date format")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToIntegerCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToIntegerCodecTest.java new file mode 100644 index 0000000..9a37493 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToIntegerCodecTest.java @@ -0,0 +1,101 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToIntegerCodecTest { + + private JsonNodeToIntegerCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToIntegerCodec) + codecFactory.createConvertingCodec( + DataTypes.INT, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0)) + .toInternal(0) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(2_147_483_647)) + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(-2_147_483_648)) + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0")) + .toInternal(0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2147483647")) + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-2147483648")) + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2,147,483,647")) + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-2,147,483,648")) + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal(0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal(1) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal(0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0) + .toExternal(JSON_NODE_FACTORY.numberNode(0)) + .convertsFromInternal(Integer.MAX_VALUE) + .toExternal(JSON_NODE_FACTORY.numberNode(2_147_483_647)) + .convertsFromInternal(Integer.MIN_VALUE) + .toExternal(JSON_NODE_FACTORY.numberNode(-2_147_483_648)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid integer")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("1.2")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("2147483648")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("-2147483649")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) // overflow + ; + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToListCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToListCodecTest.java new file mode 100644 index 0000000..6846a6f --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToListCodecTest.java @@ -0,0 +1,160 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToListCodecTest { + + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + + private JsonNodeToListCodec codec1; + + private JsonNodeToListCodec codec2; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec1 = + (JsonNodeToListCodec) + codecFactory.>createConvertingCodec( + DataTypes.listOf(DataTypes.DOUBLE), JSON_NODE_TYPE, true); + codec2 = + (JsonNodeToListCodec) + codecFactory.>createConvertingCodec( + DataTypes.listOf(DataTypes.TEXT), JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(codec1) + .convertsFromExternal(objectMapper.readTree("[1,2,3]")) + .toInternal(Lists.newArrayList(1d, 2d, 3d)) + .convertsFromExternal(objectMapper.readTree(" [ 1 , 2 , 3 ] ")) + .toInternal(Lists.newArrayList(1d, 2d, 3d)) + .convertsFromExternal(objectMapper.readTree("[1234.56,78900]")) + .toInternal(Lists.newArrayList(1234.56d, 78900d)) + .convertsFromExternal(objectMapper.readTree("[\"1,234.56\",\"78,900\"]")) + .toInternal(Lists.newArrayList(1234.56d, 78900d)) + .convertsFromExternal(objectMapper.readTree("[,]")) + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("[]")) + .toInternal(ImmutableList.of()) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + assertThat(codec2) + .convertsFromExternal(objectMapper.readTree("[\"foo\",\"bar\"]")) + .toInternal(Lists.newArrayList("foo", "bar")) + .convertsFromExternal(objectMapper.readTree("['foo','bar']")) + .toInternal(Lists.newArrayList("foo", "bar")) + .convertsFromExternal(objectMapper.readTree(" [ \"foo\" , \"bar\" ] ")) + .toInternal(Lists.newArrayList("foo", "bar")) + .convertsFromExternal(objectMapper.readTree("[ \"\\\"foo\\\"\" , \"\\\"bar\\\"\" ]")) + .toInternal(Lists.newArrayList("\"foo\"", "\"bar\"")) + .convertsFromExternal( + objectMapper.readTree("[ \"\\\"fo\\\\o\\\"\" , \"\\\"ba\\\\r\\\"\" ]")) + .toInternal(Lists.newArrayList("\"fo\\o\"", "\"ba\\r\"")) + .convertsFromExternal(objectMapper.readTree("[,]")) + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal(objectMapper.readTree("[null,null]")) + .toInternal(Lists.newArrayList(null, null)) + // DAT-297: don't apply nullStrings to inner elements + .convertsFromExternal(objectMapper.readTree("[\"\",\"\"]")) + .toInternal(Lists.newArrayList("", "")) + .convertsFromExternal(objectMapper.readTree("['','']")) + .toInternal(Lists.newArrayList("", "")) + .convertsFromExternal(objectMapper.readTree("[\"NULL\",\"NULL\"]")) + .toInternal(Lists.newArrayList("NULL", "NULL")) + .convertsFromExternal(objectMapper.readTree("['NULL','NULL']")) + .toInternal(Lists.newArrayList("NULL", "NULL")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("[]")) + .toInternal(ImmutableList.of()) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(codec1) + .convertsFromInternal(Lists.newArrayList(1d, 2d, 3d)) + .toExternal(objectMapper.readTree("[1.0,2.0,3.0]")) + .convertsFromInternal(Lists.newArrayList(1234.56d, 78900d)) + .toExternal(objectMapper.readTree("[1234.56,78900.0]")) + .convertsFromInternal(Lists.newArrayList(1d, null)) + .toExternal(objectMapper.readTree("[1.0,null]")) + .convertsFromInternal(Lists.newArrayList(null, 0d)) + .toExternal(objectMapper.readTree("[null,0.0]")) + .convertsFromInternal(Lists.newArrayList(null, null)) + .toExternal(objectMapper.readTree("[null,null]")) + .convertsFromInternal(ImmutableList.of()) + .toExternal(objectMapper.readTree("[]")) + .convertsFromInternal(null) + .toExternal(null); + assertThat(codec2) + .convertsFromInternal(Lists.newArrayList("foo", "bar")) + .toExternal(objectMapper.readTree("[\"foo\",\"bar\"]")) + .convertsFromInternal(Lists.newArrayList("\"foo\"", "\"bar\"")) + .toExternal(objectMapper.readTree("[\"\\\"foo\\\"\",\"\\\"bar\\\"\"]")) + .convertsFromInternal(Lists.newArrayList("\\foo\\", "\\bar\\")) + .toExternal(objectMapper.readTree("[\"\\\\foo\\\\\",\"\\\\bar\\\\\"]")) + .convertsFromInternal(Lists.newArrayList(",foo,", ",bar,")) + .toExternal(objectMapper.readTree("[\",foo,\",\",bar,\"]")) + .convertsFromInternal(Lists.newArrayList("", "")) + .toExternal(objectMapper.readTree("[\"\",\"\"]")) + .convertsFromInternal(Lists.newArrayList(null, null)) + .toExternal(objectMapper.readTree("[null,null]")) + .convertsFromInternal(ImmutableList.of()) + .toExternal(objectMapper.readTree("[]")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() throws Exception { + assertThat(codec1) + .cannotConvertFromExternal(objectMapper.readTree("[1,\"not a valid double\"]")); + assertThat(codec1) + .cannotConvertFromExternal(objectMapper.readTree("{ \"not a valid array\" : 42 }")); + assertThat(codec1).cannotConvertFromExternal(objectMapper.readTree("42")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalDateCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalDateCodecTest.java new file mode 100644 index 0000000..157e122 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalDateCodecTest.java @@ -0,0 +1,125 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToLocalDateCodecTest { + + private TemporalFormat format1 = + CodecUtils.getTemporalFormat( + "ISO_LOCAL_DATE", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format2 = + CodecUtils.getTemporalFormat( + "yyyyMMdd", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format3 = + CodecUtils.getTemporalFormat( + "UNITS_SINCE_EPOCH", + UTC, + US, + DAYS, + ZonedDateTime.parse("2000-01-01T00:00:00Z"), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private final List nullStrings = Lists.newArrayList("NULL"); + + @Test + void should_convert_from_valid_external() { + JsonNodeToLocalDateCodec codec = new JsonNodeToLocalDateCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2016-07-24")) + .toInternal(LocalDate.parse("2016-07-24")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + codec = new JsonNodeToLocalDateCodec(format2, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("20160724")) + .toInternal(LocalDate.parse("2016-07-24")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + codec = new JsonNodeToLocalDateCodec(format3, UTC, nullStrings); + // 12 full days after year 2000 = 2000-01-13 (at midnight) + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(12)) + .toInternal(LocalDate.parse("2000-01-13")); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToLocalDateCodec codec = new JsonNodeToLocalDateCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalDate.parse("2016-07-24")) + .toExternal(JSON_NODE_FACTORY.textNode("2016-07-24")) + .convertsFromInternal(null) + .toExternal(null); + codec = new JsonNodeToLocalDateCodec(format2, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalDate.parse("2016-07-24")) + .toExternal(JSON_NODE_FACTORY.textNode("20160724")) + .convertsFromInternal(null) + .toExternal(null); + codec = new JsonNodeToLocalDateCodec(format3, UTC, nullStrings); + // 12 full days after year 2000 = 2000-01-13 (at midnight) + assertThat(codec) + .convertsFromInternal(LocalDate.parse("2000-01-13")) + .toExternal(JSON_NODE_FACTORY.numberNode(12L)); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToLocalDateCodec codec = new JsonNodeToLocalDateCodec(format1, UTC, nullStrings); + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid date format")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalTimeCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalTimeCodecTest.java new file mode 100644 index 0000000..f436df9 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLocalTimeCodecTest.java @@ -0,0 +1,128 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.SimpleTemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToLocalTimeCodecTest { + + private TemporalFormat format1 = + CodecUtils.getTemporalFormat( + "ISO_LOCAL_TIME", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format2 = + CodecUtils.getTemporalFormat( + "HHmmss.SSS", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format3 = + CodecUtils.getTemporalFormat( + "UNITS_SINCE_EPOCH", + UTC, + US, + MINUTES, + ZonedDateTime.parse("2000-01-01T00:00:00Z"), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private final List nullStrings = Lists.newArrayList("NULL"); + + @Test + void should_convert_from_valid_external() { + JsonNodeToLocalTimeCodec codec = new JsonNodeToLocalTimeCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("12:24:46")) + .toInternal(LocalTime.parse("12:24:46")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("12:24:46.999")) + .toInternal(LocalTime.parse("12:24:46.999")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + codec = new JsonNodeToLocalTimeCodec(format2, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("122446.999")) + .toInternal(LocalTime.parse("12:24:46.999")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + codec = new JsonNodeToLocalTimeCodec(format3, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(123)) + .toInternal(LocalTime.parse("02:03:00")); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToLocalTimeCodec codec = new JsonNodeToLocalTimeCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalTime.parse("12:24:46.999")) + .toExternal(JSON_NODE_FACTORY.textNode("12:24:46.999")) + .convertsFromInternal(null) + .toExternal(null); + codec = new JsonNodeToLocalTimeCodec(format2, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalTime.parse("12:24:46.999")) + .toExternal(JSON_NODE_FACTORY.textNode("122446.999")) + .convertsFromInternal(null) + .toExternal(null); + codec = new JsonNodeToLocalTimeCodec(format3, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalTime.parse("02:03:00")) + .toExternal(JSON_NODE_FACTORY.numberNode(123L)); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToLocalTimeCodec codec = + new JsonNodeToLocalTimeCodec(new SimpleTemporalFormat(ISO_LOCAL_DATE), UTC, nullStrings); + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid date format")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLongCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLongCodecTest.java new file mode 100644 index 0000000..b2ef514 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToLongCodecTest.java @@ -0,0 +1,101 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToLongCodecTest { + + private JsonNodeToLongCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToLongCodec) + codecFactory.createConvertingCodec( + DataTypes.BIGINT, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(0L)) + .toInternal(0L) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(9_223_372_036_854_775_807L)) + .toInternal(Long.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode(-9_223_372_036_854_775_808L)) + .toInternal(Long.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0")) + .toInternal(0L) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("9223372036854775807")) + .toInternal(Long.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-9223372036854775808")) + .toInternal(Long.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("9,223,372,036,854,775,807")) + .toInternal(Long.MAX_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-9,223,372,036,854,775,808")) + .toInternal(Long.MIN_VALUE) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal(0L) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) + .toInternal(946684800000L) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal(1L) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal(0L) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0L) + .toExternal(JSON_NODE_FACTORY.numberNode(0L)) + .convertsFromInternal(Long.MAX_VALUE) + .toExternal(JSON_NODE_FACTORY.numberNode(9_223_372_036_854_775_807L)) + .convertsFromInternal(Long.MIN_VALUE) + .toExternal(JSON_NODE_FACTORY.numberNode(-9_223_372_036_854_775_808L)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid long")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("1.2")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("9223372036854775808")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("-9223372036854775809")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToMapCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToMapCodecTest.java new file mode 100644 index 0000000..e36ec5f --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToMapCodecTest.java @@ -0,0 +1,124 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToMapCodecTest { + + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + + private JsonNodeToMapCodec> codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext() + .setNullStrings("NULL") + .setFormatNumbers(true) + .setRoundingMode(HALF_EVEN); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToMapCodec>) + codecFactory.>>createConvertingCodec( + DataTypes.mapOf(DataTypes.DOUBLE, DataTypes.listOf(DataTypes.TEXT)), + JSON_NODE_TYPE, + true); + } + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(codec) + .convertsFromExternal(objectMapper.readTree("{1 : [\"foo\", \"bar\"], 2:[\"qix\"]}")) + .toInternal(map(1d, list("foo", "bar"), 2d, list("qix"))) + .convertsFromExternal( + objectMapper.readTree("{ '1234.56' : ['foo', 'bar'], '0.12' : ['qix'] }")) + .toInternal(map(1234.56d, list("foo", "bar"), 0.12d, list("qix"))) + .convertsFromExternal(objectMapper.readTree("{ '1,234.56' : ['foo'] , '.12' : ['bar']}")) + .toInternal(map(1234.56d, list("foo"), 0.12d, list("bar"))) + .convertsFromExternal(objectMapper.readTree("{1: [], 2 :[]}")) + .toInternal(map(1d, list(), 2d, list())) + // DAT-297: don't apply nullStrings to inner elements + .convertsFromExternal(objectMapper.readTree("{1: [\"NULL\"], 2: ['NULL']}")) + .toInternal(map(1d, list("NULL"), 2d, list("NULL"))) + .convertsFromExternal(objectMapper.readTree("{1: [\"\"], 2: ['']}")) + .toInternal(map(1d, list(""), 2d, list(""))) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("{}")) + .toInternal(ImmutableMap.of()) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(codec) + .convertsFromInternal(map(1d, list("foo", "bar"), 2d, list("qix"))) + .toExternal(objectMapper.readTree("{\"1\":[\"foo\",\"bar\"],\"2\":[\"qix\"]}")) + .convertsFromInternal(map(1234.56d, list("foo", "bar"), 0.12d, list("qix"))) + .toExternal(objectMapper.readTree("{\"1,234.56\":[\"foo\",\"bar\"],\"0.12\":[\"qix\"]}")) + .convertsFromInternal(map(1d, list(""), 2d, list(""))) + .toExternal(objectMapper.readTree("{\"1\":[\"\"],\"2\":[\"\"]}")) + .convertsFromInternal(map(1d, null, 2d, list())) + .toExternal(objectMapper.readTree("{\"1\":null,\"2\":[]}")) + .convertsFromInternal(ImmutableMap.of()) + .toExternal(objectMapper.readTree("{}")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() throws Exception { + assertThat(codec) + .cannotConvertFromExternal(objectMapper.readTree("{\"not a valid input\":\"foo\"}")) + .cannotConvertFromExternal(objectMapper.readTree("[1,\"not a valid object\"]")) + .cannotConvertFromExternal(objectMapper.readTree("42")); + } + + private static Map> map( + Double k1, List v1, Double k2, List v2) { + Map> map = new LinkedHashMap<>(); + map.put(k1, v1); + map.put(k2, v2); + return map; + } + + private static List list(String... elements) { + return Arrays.asList(elements); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToSetCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToSetCodecTest.java new file mode 100644 index 0000000..f80f473 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToSetCodecTest.java @@ -0,0 +1,157 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Set; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToSetCodecTest { + + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + + private JsonNodeToSetCodec codec1; + private JsonNodeToSetCodec codec2; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec1 = + (JsonNodeToSetCodec) + codecFactory.>createConvertingCodec( + DataTypes.setOf(DataTypes.DOUBLE), JSON_NODE_TYPE, true); + codec2 = + (JsonNodeToSetCodec) + codecFactory.>createConvertingCodec( + DataTypes.setOf(DataTypes.TEXT), JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(codec1) + .convertsFromExternal(objectMapper.readTree("[1,2,3]")) + .toInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .convertsFromExternal(objectMapper.readTree(" [ 1 , 2 , 3 ] ")) + .toInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .convertsFromExternal(objectMapper.readTree("[1234.56,78900]")) + .toInternal(Sets.newLinkedHashSet(1234.56d, 78900d)) + .convertsFromExternal(objectMapper.readTree("[\"1,234.56\",\"78,900\"]")) + .toInternal(Sets.newLinkedHashSet(1234.56d, 78900d)) + .convertsFromExternal(objectMapper.readTree("[,]")) + .toInternal(Sets.newLinkedHashSet(null, null)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("[]")) + .toInternal(Sets.newLinkedHashSet()) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + assertThat(codec2) + .convertsFromExternal(objectMapper.readTree("[\"foo\",\"bar\"]")) + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal(objectMapper.readTree("['foo','bar']")) + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal(objectMapper.readTree(" [ \"foo\" , \"bar\" ] ")) + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal(objectMapper.readTree("[ \"\\\"foo\\\"\" , \"\\\"bar\\\"\" ]")) + .toInternal(Sets.newLinkedHashSet("\"foo\"", "\"bar\"")) + .convertsFromExternal( + objectMapper.readTree("[ \"\\\"fo\\\\o\\\"\" , \"\\\"ba\\\\r\\\"\" ]")) + .toInternal(Sets.newLinkedHashSet("\"fo\\o\"", "\"ba\\r\"")) + .convertsFromExternal(objectMapper.readTree("[,]")) + .toInternal(Sets.newLinkedHashSet(null, null)) + .convertsFromExternal(objectMapper.readTree("[null,null]")) + .toInternal(Sets.newLinkedHashSet(null, null)) + // DAT-297: don't apply nullStrings to inner elements + .convertsFromExternal(objectMapper.readTree("[\"\",\"\"]")) + .toInternal(Sets.newLinkedHashSet("")) + .convertsFromExternal(objectMapper.readTree("['','']")) + .toInternal(Sets.newLinkedHashSet("")) + .convertsFromExternal(objectMapper.readTree("[\"NULL\",\"NULL\"]")) + .toInternal(Sets.newLinkedHashSet("NULL")) + .convertsFromExternal(objectMapper.readTree("['NULL','NULL']")) + .toInternal(Sets.newLinkedHashSet("NULL")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("[]")) + .toInternal(Sets.newLinkedHashSet()) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(codec1) + .convertsFromInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .toExternal(objectMapper.readTree("[1.0,2.0,3.0]")) + .convertsFromInternal(Sets.newLinkedHashSet(1234.56d, 78900d)) + .toExternal(objectMapper.readTree("[1234.56,78900.0]")) + .convertsFromInternal(Sets.newLinkedHashSet(1d, null)) + .toExternal(objectMapper.readTree("[1.0,null]")) + .convertsFromInternal(Sets.newLinkedHashSet(null, 0d)) + .toExternal(objectMapper.readTree("[null,0.0]")) + .convertsFromInternal(Sets.newLinkedHashSet((Double) null)) + .toExternal(objectMapper.readTree("[null]")) + .convertsFromInternal(Sets.newLinkedHashSet()) + .toExternal(objectMapper.readTree("[]")) + .convertsFromInternal(null) + .toExternal(null); + assertThat(codec2) + .convertsFromInternal(Sets.newLinkedHashSet("foo", "bar")) + .toExternal(objectMapper.readTree("[\"foo\",\"bar\"]")) + .convertsFromInternal(Sets.newLinkedHashSet("\"foo\"", "\"bar\"")) + .toExternal(objectMapper.readTree("[\"\\\"foo\\\"\",\"\\\"bar\\\"\"]")) + .convertsFromInternal(Sets.newLinkedHashSet("\\foo\\", "\\bar\\")) + .toExternal(objectMapper.readTree("[\"\\\\foo\\\\\",\"\\\\bar\\\\\"]")) + .convertsFromInternal(Sets.newLinkedHashSet(",foo,", ",bar,")) + .toExternal(objectMapper.readTree("[\",foo,\",\",bar,\"]")) + .convertsFromInternal(Sets.newLinkedHashSet("")) + .toExternal(objectMapper.readTree("[\"\"]")) + .convertsFromInternal(Sets.newLinkedHashSet((String) null)) + .toExternal(objectMapper.readTree("[null]")) + .convertsFromInternal(Sets.newLinkedHashSet()) + .toExternal(objectMapper.readTree("[]")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() throws Exception { + assertThat(codec1) + .cannotConvertFromExternal(objectMapper.readTree("[1,\"not a valid double\"]")) + .cannotConvertFromExternal(objectMapper.readTree("{ \"not a valid array\" : 42 }")) + .cannotConvertFromExternal(objectMapper.readTree("42")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToShortCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToShortCodecTest.java new file mode 100644 index 0000000..9205b9b --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToShortCodecTest.java @@ -0,0 +1,101 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToShortCodecTest { + + private JsonNodeToShortCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (JsonNodeToShortCodec) + codecFactory.createConvertingCodec( + DataTypes.SMALLINT, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode((short) 0)) + .toInternal((short) 0) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode((short) 32767)) + .toInternal((short) 32767) + .convertsFromExternal(JSON_NODE_FACTORY.numberNode((short) -32768)) + .toInternal((short) -32768) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("0")) + .toInternal((short) 0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("32767")) + .toInternal((short) 32767) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-32768")) + .toInternal((short) -32768) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("32,767")) + .toInternal((short) 32767) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("-32,768")) + .toInternal((short) -32768) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("1970-01-01T00:00:00Z")) + .toInternal((short) 0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("TRUE")) + .toInternal((short) 1) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("FALSE")) + .toInternal((short) 0) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal((short) 0) + .toExternal(JSON_NODE_FACTORY.numberNode((short) 0)) + .convertsFromInternal(Short.MAX_VALUE) + .toExternal(JSON_NODE_FACTORY.numberNode((short) 32_767)) + .convertsFromInternal(Short.MIN_VALUE) + .toExternal(JSON_NODE_FACTORY.numberNode((short) -32_768)) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid short")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("1.2")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("32768")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("-32769")) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("2000-01-01T00:00:00Z")) // overflow + ; + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToStringCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToStringCodecTest.java new file mode 100644 index 0000000..e9e4f0f --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToStringCodecTest.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToStringCodecTest { + + private List nullStrings = Lists.newArrayList("NULL"); + private ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + private JsonNodeFactory nodeFactory = objectMapper.getNodeFactory(); + + @Test + void should_convert_from_valid_external() throws IOException { + JsonNodeToStringCodec codec = + new JsonNodeToStringCodec(TypeCodecs.TEXT, objectMapper, nullStrings); + assertThat(codec) + .convertsFromExternal(nodeFactory.textNode("foo")) + .toInternal("foo") + .convertsFromExternal(nodeFactory.textNode("\"foo\"")) + .toInternal("\"foo\"") + .convertsFromExternal(nodeFactory.textNode("'foo'")) + .toInternal("'foo'") + .convertsFromExternal(nodeFactory.numberNode(42)) + .toInternal("42") + .convertsFromExternal(objectMapper.readTree("{\"foo\":42}")) + .toInternal("{\"foo\":42}") + .convertsFromExternal(objectMapper.readTree("[1,2,3]")) + .toInternal("[1,2,3]") + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(nodeFactory.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(nodeFactory.textNode("")) + .toInternal("") // empty string should nto be converted to null + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToStringCodec codec = + new JsonNodeToStringCodec(TypeCodecs.TEXT, objectMapper, nullStrings); + assertThat(codec) + .convertsFromInternal("foo") + .toExternal(nodeFactory.textNode("foo")) + .convertsFromInternal("\"foo\"") + .toExternal(nodeFactory.textNode("\"foo\"")) + .convertsFromInternal("'foo'") + .toExternal(nodeFactory.textNode("'foo'")) + .convertsFromInternal("42") + .toExternal(nodeFactory.textNode("42")) + .convertsFromInternal("42 abc") + .toExternal(nodeFactory.textNode("42 abc")) + .convertsFromInternal("{\"foo\":42}") + .toExternal(nodeFactory.textNode("{\"foo\":42}")) + .convertsFromInternal("[1,2,3]") + .toExternal(nodeFactory.textNode("[1,2,3]")) + .convertsFromInternal("{\"foo\":42") // invalid json + .toExternal(nodeFactory.textNode("{\"foo\":42")) + .convertsFromInternal(null) + .toExternal(null) + .convertsFromInternal("") + .toExternal(nodeFactory.textNode("")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTupleCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTupleCodecTest.java new file mode 100644 index 0000000..66b8353 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToTupleCodecTest.java @@ -0,0 +1,170 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.datastax.oss.dsbulk.tests.driver.DriverUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToTupleCodecTest { + + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + + private TupleType tupleType; + + private JsonNodeToTupleCodec codec1; + private JsonNodeToTupleCodec codec2; + private JsonNodeToTupleCodec codec3; + + @BeforeEach + void setUp() { + tupleType = + DriverUtils.mockTupleType( + DefaultProtocolVersion.V4, CodecRegistry.DEFAULT, DataTypes.TIMESTAMP, DataTypes.TEXT); + ConversionContext context1 = new TextConversionContext().setNullStrings("NULL", ""); + ConversionContext context2 = new TextConversionContext().setAllowExtraFields(true); + ConversionContext context3 = new TextConversionContext().setAllowMissingFields(true); + codec1 = + (JsonNodeToTupleCodec) + new ConvertingCodecFactory(context1) + .createConvertingCodec(tupleType, JSON_NODE_TYPE, true); + codec2 = + (JsonNodeToTupleCodec) + new ConvertingCodecFactory(context2) + .createConvertingCodec(tupleType, JSON_NODE_TYPE, true); + codec3 = + (JsonNodeToTupleCodec) + new ConvertingCodecFactory(context3) + .createConvertingCodec(tupleType, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(codec1) + .convertsFromExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999\",\"+01:00\"]")) + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal(objectMapper.readTree("['2016-07-24T20:34:12.999','+01:00']")) + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal(objectMapper.readTree("[ \"2016-07-24T20:34:12.999\" , \"+01:00\" ]")) + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\",\"+01:00\"]")) + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal(objectMapper.readTree("[,\"\"]")) + .toInternal(tupleType.newValue(null, "")) + .convertsFromExternal(objectMapper.readTree("[,\"NULL\"]")) + .toInternal(tupleType.newValue(null, "NULL")) + .convertsFromExternal(objectMapper.readTree("[null,null]")) + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal(objectMapper.readTree("[,]")) + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + // should allow extra elements + assertThat(codec2) + .convertsFromExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999\",\"+01:00\", 42]")) + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal(objectMapper.readTree("[,\"\",\"\"]")) + .toInternal(tupleType.newValue(null, "")) + .convertsFromExternal(objectMapper.readTree("[null,null,null]")) + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal(objectMapper.readTree("[,,]")) + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + // should allow missing elements + assertThat(codec3) + .convertsFromExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999\"]")) + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), null)) + .convertsFromExternal(objectMapper.readTree("[null]")) + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal(objectMapper.readTree("[]")) + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(codec1) + .convertsFromInternal( + tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .toExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\",\"+01:00\"]")) + .convertsFromInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "")) + .toExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\",\"\"]")) + .convertsFromInternal(tupleType.newValue(null, "")) + .toExternal(objectMapper.readTree("[null,\"\"]")) + .convertsFromInternal(tupleType.newValue(null, null)) + .toExternal(objectMapper.readTree("[null,null]")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() throws Exception { + assertThat(codec1) + .cannotConvertFromExternal(objectMapper.readTree("{\"not a valid tuple\":42}")); + // should not allow missing elements + assertThat(codec2) + .cannotConvertFromExternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\"]")) + .cannotConvertFromExternal(objectMapper.readTree("[]")); + // should not allow extra elements + assertThat(codec3) + .cannotConvertFromExternal( + objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\",\"+01:00\",42]")); + // tests for error messages + assertThatThrownBy( + () -> + codec1.externalToInternal(objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\"]"))) + .isInstanceOf(JsonSchemaMismatchException.class) + .hasMessageContaining("expecting 2 elements, got 1"); + assertThatThrownBy( + () -> + codec1.externalToInternal( + objectMapper.readTree("[\"2016-07-24T20:34:12.999Z\",\"+01:00\",42]"))) + .isInstanceOf(JsonSchemaMismatchException.class) + .hasMessageContaining("expecting 2 elements, got 3"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUDTCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUDTCodecTest.java new file mode 100644 index 0000000..91e738c --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUDTCodecTest.java @@ -0,0 +1,265 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_TYPE; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonNodeToUDTCodecTest { + + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + + // user types + + private final UserDefinedType udt1 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("\"F1A\"", DataTypes.INT) + .withField("f1b", DataTypes.mapOf(DataTypes.TEXT, DataTypes.DOUBLE)) + .build(); + + private final UserDefinedType udt2 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("f2a", udt1) + .withField("f2b", DataTypes.listOf(DataTypes.DATE)) + .build(); + + private final UserDefinedType udt3 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("f1", DataTypes.INT) + .withField("f2", DataTypes.INT) + .build(); + + private final UserDefinedType udt4 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("f1", DataTypes.INT) + .withField("f2", DataTypes.INT) + .build(); + + // user type values + + private final UdtValue udt1Value = + udt1.newValue() + .setInt("\"F1A\"", 42) + .setMap("f1b", newMap("foo", 1234.56d, "", 0.12d), String.class, Double.class); + private final UdtValue udt1ValueEmpty = udt1.newValue().setToNull("\"F1A\"").setToNull("f1b"); + + private final UdtValue udt2Value = + udt2.newValue() + .setUdtValue("f2a", udt1Value) + .set("f2b", newList(LocalDate.of(2017, 9, 22)), TypeCodecs.listOf(TypeCodecs.DATE)); + private final UdtValue udt2ValueEmpty = udt2.newValue().setToNull("f2a").setToNull("f2b"); + + private final UdtValue udt3Value = udt3.newValue().setInt("f1", 42).setInt("f2", 42); + + private final UdtValue udt4Value = udt4.newValue().setInt("f1", 42).setInt("f2", 42); + private final UdtValue udt4ValuePartial = udt4.newValue().setInt("f1", 42); + private final UdtValue udt4ValueEmpty = udt4.newValue(); + + // codecs + + private JsonNodeToUDTCodec udtCodec1; + private JsonNodeToUDTCodec udtCodec2; + private JsonNodeToUDTCodec udtCodec3; + private JsonNodeToUDTCodec udtCodec4; + + @BeforeEach + void setUp() { + ConversionContext context1 = new TextConversionContext().setNullStrings("NULL", ""); + ConversionContext context2 = new TextConversionContext().setAllowExtraFields(true); + ConversionContext context3 = new TextConversionContext().setAllowMissingFields(true); + ConvertingCodecFactory codecFactory1 = new ConvertingCodecFactory(context1); + ConvertingCodecFactory codecFactory2 = new ConvertingCodecFactory(context2); + ConvertingCodecFactory codecFactory3 = new ConvertingCodecFactory(context3); + udtCodec1 = + (JsonNodeToUDTCodec) + codecFactory1.createConvertingCodec(udt1, JSON_NODE_TYPE, true); + udtCodec2 = + (JsonNodeToUDTCodec) + codecFactory1.createConvertingCodec(udt2, JSON_NODE_TYPE, true); + udtCodec3 = + (JsonNodeToUDTCodec) + codecFactory2.createConvertingCodec(udt3, JSON_NODE_TYPE, true); + udtCodec4 = + (JsonNodeToUDTCodec) + codecFactory3.createConvertingCodec(udt4, JSON_NODE_TYPE, true); + } + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(udtCodec1) + .convertsFromExternal( + objectMapper.readTree("{\"F1A\":42,\"f1b\":{\"foo\":1234.56,\"\":0.12}}")) + .toInternal(udt1Value) + .convertsFromExternal(objectMapper.readTree("{'F1A':42,'f1b':{'foo':1234.56,'':0.12}}")) + .toInternal(udt1Value) + .convertsFromExternal( + objectMapper.readTree( + "{ \"f1b\" : { \"foo\" : \"1,234.56\" , \"\" : \"0000.12000\" } , \"F1A\" : \"42.00\" }")) + .toInternal(udt1Value) + .convertsFromExternal(objectMapper.readTree("[42,{\"foo\":1234.56,\"\":0.12}]")) + .toInternal(udt1Value) + .convertsFromExternal(objectMapper.readTree("{ \"f1b\" : { } , \"F1A\" : null }")) + .toInternal(udt1ValueEmpty) + .convertsFromExternal(objectMapper.readTree("{ \"f1b\" : null , \"F1A\" : null }")) + .toInternal(udt1ValueEmpty) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + assertThat(udtCodec2) + .convertsFromExternal( + objectMapper.readTree( + "{\"f2a\":{\"F1A\":42,\"f1b\":{\"foo\":1234.56,\"\":0.12}},\"f2b\":[\"2017-09-22\"]}")) + .toInternal(udt2Value) + .convertsFromExternal( + objectMapper.readTree( + "{'f2a':{'F1A':42,'f1b':{'foo':1234.56,'':0.12}},'f2b':['2017-09-22']}")) + .toInternal(udt2Value) + .convertsFromExternal( + objectMapper.readTree( + "{ \"f2b\" : [ \"2017-09-22\" ] , \"f2a\" : { \"f1b\" : { \"foo\" : \"1,234.56\" , \"\" : \"0000.12000\" } , \"F1A\" : \"42.00\" } }")) + .toInternal(udt2Value) + .convertsFromExternal(objectMapper.readTree("{ \"f2b\" : null , \"f2a\" : null }")) + .toInternal(udt2ValueEmpty) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + // should allow extra fields + assertThat(udtCodec3) + .convertsFromExternal(objectMapper.readTree("{\"f1\":42,\"f2\":42}")) + .toInternal(udt3Value) + .convertsFromExternal(objectMapper.readTree("{\"f1\":42,\"f2\":42,\"f3\":42}")) + .toInternal(udt3Value) + .convertsFromExternal(objectMapper.readTree("[42,42]")) + .toInternal(udt3Value) + .convertsFromExternal(objectMapper.readTree("[42,42,42]")) + .toInternal(udt3Value) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + // should allow missing fields + assertThat(udtCodec4) + .convertsFromExternal(objectMapper.readTree("{\"f1\":42,\"f2\":42}")) + .toInternal(udt4Value) + .convertsFromExternal(objectMapper.readTree("{\"f1\":42}")) + .toInternal(udt4ValuePartial) + .convertsFromExternal(objectMapper.readTree("{}")) + .toInternal(udt4ValueEmpty) + .convertsFromExternal(objectMapper.readTree("[42,42]")) + .toInternal(udt4Value) + .convertsFromExternal(objectMapper.readTree("[42]")) + .toInternal(udt4ValuePartial) + .convertsFromExternal(objectMapper.readTree("[]")) + .toInternal(udt4ValueEmpty) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(objectMapper.readTree("")) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(udtCodec1) + .convertsFromInternal(udt1Value) + .toExternal(objectMapper.readTree("{\"F1A\":42,\"f1b\":{\"foo\":1234.56,\"\":0.12}}")) + .convertsFromInternal(udt1.newValue()) + .toExternal(objectMapper.readTree("{\"F1A\":null,\"f1b\":{}}")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() throws Exception { + assertThat(udtCodec1) + .cannotConvertFromExternal(objectMapper.readTree("42")) + .cannotConvertFromExternal(objectMapper.readTree("{\"F1A\":42}")) + .cannotConvertFromExternal(objectMapper.readTree("[42]")) + .cannotConvertFromExternal(objectMapper.readTree("{\"F1A\":null,\"f1c\":{}}")) + .cannotConvertFromExternal(objectMapper.readTree("{\"not a valid input\":\"foo\"}")); + // should not allow missing fields + assertThat(udtCodec3) + .cannotConvertFromExternal(objectMapper.readTree("{\"f1\":42}")) + .cannotConvertFromExternal(objectMapper.readTree("[42]")) + .cannotConvertFromExternal(objectMapper.readTree("[]")); + // should not allow extra fields + assertThat(udtCodec4) + .cannotConvertFromExternal(objectMapper.readTree("{\"f1\":42,\"f2\":42,\"f3\":42}")) + .cannotConvertFromExternal(objectMapper.readTree("[42,42,42]")); + // tests for error messages + assertThatThrownBy( + () -> + udtCodec1.externalToInternal(objectMapper.readTree("{\"F1A\":null,\"f1c\":null}"))) + .isInstanceOf(JsonSchemaMismatchException.class) + .hasMessageContaining("1 extraneous field: 'f1c'") + .hasMessageContaining("1 missing field: 'f1b'"); + assertThatThrownBy( + () -> udtCodec3.externalToInternal(objectMapper.readTree("{\"f1\":null,\"f3\":null}"))) + .isInstanceOf(JsonSchemaMismatchException.class) + .satisfies( + t -> + assertThat(t.getMessage()) + .contains("1 missing field: 'f2'") + .doesNotContain("1 extraneous field: 'f3'")); + assertThatThrownBy( + () -> udtCodec4.externalToInternal(objectMapper.readTree("{\"f1\":null,\"f3\":null}"))) + .isInstanceOf(JsonSchemaMismatchException.class) + .satisfies( + t -> + assertThat(t.getMessage()) + .contains("1 extraneous field: 'f3'") + .doesNotContain("1 missing field: 'f2'")); + } + + @SuppressWarnings("SameParameterValue") + private static Map newMap(String k1, Double v1, String k2, Double v2) { + Map map = new LinkedHashMap<>(); + map.put(k1, v1); + map.put(k2, v2); + return map; + } + + private static List newList(LocalDate... elements) { + return Arrays.asList(elements); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUUIDCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUUIDCodecTest.java new file mode 100644 index 0000000..df2b9b9 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUUIDCodecTest.java @@ -0,0 +1,122 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.TimeUUIDGenerator; +import com.datastax.oss.dsbulk.codecs.text.string.StringToInstantCodec; +import io.netty.util.concurrent.FastThreadLocal; +import java.text.NumberFormat; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class JsonNodeToUUIDCodecTest { + + private final FastThreadLocal numberFormat = + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true); + + private final List nullStrings = Lists.newArrayList("NULL"); + + private StringToInstantCodec instantCodec = + new StringToInstantCodec( + CodecUtils.getTemporalFormat( + "yyyy-MM-dd'T'HH:mm:ss[.SSSSSSSSS]XXX", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + numberFormat, + true), + UTC, + EPOCH.atZone(UTC), + nullStrings); + + private final JsonNodeToUUIDCodec codec = + new JsonNodeToUUIDCodec(TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.MIN, nullStrings); + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("a15341ec-ebef-4eab-b91d-ff16bf801a79")) + .toInternal(UUID.fromString("a15341ec-ebef-4eab-b91d-ff16bf801a79")) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null); + + assertThat( + new JsonNodeToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.MIN, nullStrings)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2017-12-05T12:44:36+01:00")) + .toInternal( + Uuids.startOf( + ZonedDateTime.parse("2017-12-05T12:44:36+01:00").toInstant().toEpochMilli())); + assertThat( + new JsonNodeToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.MAX, nullStrings)) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("2017-12-05T12:44:36.999999999+01:00")) + .toInternal( + Uuids.endOf( + ZonedDateTime.parse("2017-12-05T12:44:36.999+01:00").toInstant().toEpochMilli())); + assertThat( + new JsonNodeToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.FIXED, nullStrings) + .externalToInternal(JSON_NODE_FACTORY.textNode("2017-12-05T12:44:36+01:00")) + .timestamp()) + .isEqualTo( + Uuids.startOf( + ZonedDateTime.parse("2017-12-05T12:44:36+01:00").toInstant().toEpochMilli()) + .timestamp()); + assertThat( + new JsonNodeToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.RANDOM, nullStrings) + .externalToInternal(JSON_NODE_FACTORY.textNode("2017-12-05T12:44:36+01:00")) + .timestamp()) + .isEqualTo( + Uuids.startOf( + ZonedDateTime.parse("2017-12-05T12:44:36+01:00").toInstant().toEpochMilli()) + .timestamp()); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(UUID.fromString("a15341ec-ebef-4eab-b91d-ff16bf801a79")) + .toExternal(JSON_NODE_FACTORY.textNode("a15341ec-ebef-4eab-b91d-ff16bf801a79")) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid UUID")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUnknownTypeCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUnknownTypeCodecTest.java new file mode 100644 index 0000000..56df8e8 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToUnknownTypeCodecTest.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.driver.shaded.guava.common.collect.Lists.newArrayList; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.dsbulk.codecs.text.string.StringToUnknownTypeCodecTest.Fruit; +import com.datastax.oss.dsbulk.codecs.text.string.StringToUnknownTypeCodecTest.FruitCodec; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToUnknownTypeCodecTest { + + private FruitCodec targetCodec = new FruitCodec(); + private List nullStrings = newArrayList("NULL"); + private Fruit banana = new Fruit("banana"); + + @Test + void should_convert_from_valid_external() { + JsonNodeToUnknownTypeCodec codec = + new JsonNodeToUnknownTypeCodec<>(targetCodec, nullStrings); + assertThat(codec) + .convertsFromExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("banana")) + .toInternal(banana) + .convertsFromExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToUnknownTypeCodec codec = + new JsonNodeToUnknownTypeCodec<>(targetCodec, nullStrings); + assertThat(codec) + .convertsFromInternal(banana) + .toExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("banana")); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToUnknownTypeCodec codec = + new JsonNodeToUnknownTypeCodec<>(targetCodec, nullStrings); + assertThat(codec) + .cannotConvertFromExternal( + JsonCodecUtils.JSON_NODE_FACTORY.textNode("not a valid fruit literal")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToVectorCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToVectorCodecTest.java new file mode 100644 index 0000000..a7eea57 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonNodeToVectorCodecTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.data.CqlVector; +import com.datastax.oss.driver.api.core.type.CqlVectorType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import java.util.ArrayList; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class JsonNodeToVectorCodecTest { + private final ArrayList values = Lists.newArrayList(1.1f, 2.2f, 3.3f, 4.4f, 5.5f); + private final CqlVector vector = CqlVector.builder().addAll(values).build(); + private final CqlVectorCodec vectorCodec = + new CqlVectorCodec(new CqlVectorType(DataTypes.FLOAT, 5), TypeCodecs.FLOAT); + private final ArrayNode vectorDoc; + + private final ConvertingCodecFactory factory = new ConvertingCodecFactory(); + private final JsonNodeConvertingCodecProvider provider = new JsonNodeConvertingCodecProvider(); + private final JsonNodeToVectorCodec dsbulkCodec = + new JsonNodeToVectorCodec( + vectorCodec, + provider + .maybeProvide(DataTypes.FLOAT, GenericType.of(JsonNode.class), factory, false) + .get(), + JsonCodecUtils.getObjectMapper(), + Lists.newArrayList("NULL")); + + public JsonNodeToVectorCodecTest() { + this.vectorDoc = JSON_NODE_FACTORY.arrayNode(); + for (float value : values) { + this.vectorDoc.add(JSON_NODE_FACTORY.numberNode(value)); + } + } + + @Test + void should_convert_from_valid_external() { + assertThat(dsbulkCodec) + .convertsFromExternal(vectorDoc) // standard pattern + .toInternal(vector) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(dsbulkCodec) + .convertsFromInternal(vector) + .toExternal(vectorDoc) + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + @Disabled("Requires driver support for validating a CqlVector against a CqlVectorType") + void should_not_convert_from_invalid_internal() { + // Too few values to match dimensions + ArrayList tooMany = Lists.newArrayList(values); + tooMany.add(6.6f); + ArrayList tooFew = Lists.newArrayList(values); + tooFew.remove(0); + + assertThat(dsbulkCodec) + .cannotConvertFromInternal(CqlVector.builder().addAll(tooMany).build()) + .cannotConvertFromInternal(CqlVector.builder().addAll(tooFew).build()) + .cannotConvertFromInternal("not a valid vector"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonSchemaMismatchExceptionTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonSchemaMismatchExceptionTest.java new file mode 100644 index 0000000..546a291 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/JsonSchemaMismatchExceptionTest.java @@ -0,0 +1,121 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.Test; + +class JsonSchemaMismatchExceptionTest { + + @Test + void should_report_object_with_extraneous_and_missing_fields() { + assertThat( + JsonSchemaMismatchException.objectHasMissingAndExtraneousFields( + Sets.newLinkedHashSet("extra1"), Sets.newLinkedHashSet("missing1"))) + .hasMessage( + "JSON object does not match UDT definition: " + + "found 1 extraneous field: 'extra1' and 1 missing field: 'missing1' " + + "(set schema.allowExtraFields to true to allow JSON objects to contain fields not present " + + "in the UDT definition and set schema.allowMissingFields to true to allow JSON objects " + + "to lack of fields present in the UDT definition)."); + assertThat( + JsonSchemaMismatchException.objectHasMissingAndExtraneousFields( + Sets.newLinkedHashSet("extra1", "extra2"), + Sets.newLinkedHashSet("missing1", "missing2"))) + .hasMessage( + "JSON object does not match UDT definition: " + + "found 2 extraneous fields: 'extra1', 'extra2' and 2 missing fields: 'missing1', 'missing2' " + + "(set schema.allowExtraFields to true to allow JSON objects to contain fields not present " + + "in the UDT definition and set schema.allowMissingFields to true to allow JSON objects " + + "to lack of fields present in the UDT definition)."); + } + + @Test + void should_report_object_with_extraneous_fields() { + assertThat( + JsonSchemaMismatchException.objectHasExtraneousFields(Sets.newLinkedHashSet("extra1"))) + .hasMessage( + "JSON object does not match UDT definition: " + + "found 1 extraneous field: 'extra1' " + + "(set schema.allowExtraFields to true to allow JSON objects to contain fields not present " + + "in the UDT definition)."); + assertThat( + JsonSchemaMismatchException.objectHasExtraneousFields( + Sets.newLinkedHashSet("extra1", "extra2"))) + .hasMessage( + "JSON object does not match UDT definition: " + + "found 2 extraneous fields: 'extra1', 'extra2' " + + "(set schema.allowExtraFields to true to allow JSON objects to contain fields not present " + + "in the UDT definition)."); + } + + @Test + void should_report_object_with_missing_fields() { + assertThat( + JsonSchemaMismatchException.objectHasMissingFields(Sets.newLinkedHashSet("missing1"))) + .hasMessage( + "JSON object does not match UDT definition: " + + "found 1 missing field: 'missing1' " + + "(set schema.allowMissingFields to true to allow JSON objects " + + "to lack of fields present in the UDT definition)."); + assertThat( + JsonSchemaMismatchException.objectHasMissingFields( + Sets.newLinkedHashSet("missing1", "missing2"))) + .hasMessage( + "JSON object does not match UDT definition: " + + "found 2 missing fields: 'missing1', 'missing2' " + + "(set schema.allowMissingFields to true to allow JSON objects " + + "to lack of fields present in the UDT definition)."); + } + + @Test + void should_report_array_size_greater_than_udt_size() { + assertThat(JsonSchemaMismatchException.arraySizeGreaterThanUDTSize(3, 4)) + .hasMessage( + "JSON array does not match UDT definition: expecting 3 elements, got 4 " + + "(set schema.allowExtraFields to true to allow JSON arrays to contain " + + "more elements than the UDT definition)."); + } + + @Test + void should_report_array_size_lesser_than_udt_size() { + assertThat(JsonSchemaMismatchException.arraySizeLesserThanUDTSize(3, 2)) + .hasMessage( + "JSON array does not match UDT definition: expecting 3 elements, got 2 " + + "(set schema.allowMissingFields to true to allow JSON arrays to contain " + + "fewer elements than the UDT definition)."); + } + + @Test + void should_report_array_size_greater_than_tuple_size() { + assertThat(JsonSchemaMismatchException.arraySizeGreaterThanTupleSize(3, 4)) + .hasMessage( + "JSON array does not match tuple definition: expecting 3 elements, got 4 " + + "(set schema.allowExtraFields to true to allow JSON arrays to contain " + + "more elements than the tuple definition)."); + } + + @Test + void should_report_array_size_lesser_than_tuple_size() { + assertThat(JsonSchemaMismatchException.arraySizeLesserThanTupleSize(3, 2)) + .hasMessage( + "JSON array does not match tuple definition: expecting 3 elements, got 2 " + + "(set schema.allowMissingFields to true to allow JSON arrays to contain " + + "fewer elements than the tuple definition)."); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToDateRangeCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToDateRangeCodecTest.java new file mode 100644 index 0000000..44f3a60 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToDateRangeCodecTest.java @@ -0,0 +1,72 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.time.DateRange; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils; +import java.text.ParseException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToDateRangeCodecTest { + + private List nullStrings = Lists.newArrayList("NULL"); + private DateRange dateRange; + + JsonNodeToDateRangeCodecTest() { + try { + dateRange = DateRange.parse("[* TO 2014-12-01]"); + } catch (ParseException e) { + // swallow; can't happen. + } + } + + @Test + void should_convert_from_valid_external() { + + JsonNodeToDateRangeCodec codec = new JsonNodeToDateRangeCodec(nullStrings); + assertThat(codec) + .convertsFromExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("[* TO 2014-12-01]")) + .toInternal(dateRange) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToDateRangeCodec codec = new JsonNodeToDateRangeCodec(nullStrings); + assertThat(codec) + .convertsFromInternal(dateRange) + .toExternal(JsonCodecUtils.JSON_NODE_FACTORY.textNode("[* TO 2014-12-01]")); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToDateRangeCodec codec = new JsonNodeToDateRangeCodec(nullStrings); + assertThat(codec) + .cannotConvertFromExternal( + JsonCodecUtils.JSON_NODE_FACTORY.textNode("not a valid date range literal")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToLineStringCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToLineStringCodecTest.java new file mode 100644 index 0000000..f8a02a7 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToLineStringCodecTest.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.geometry.LineString; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultLineString; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPoint; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownBinaryGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownTextGeoFormat; +import com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToLineStringCodecTest { + + private final List nullStrings = Lists.newArrayList("NULL"); + private final LineString lineString = + new DefaultLineString( + new DefaultPoint(30, 10), new DefaultPoint(10, 30), new DefaultPoint(40, 40)); + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + private final JsonNode geoJsonNode = + objectMapper.readTree( + "{\"type\":\"LineString\",\"coordinates\":[[30.0,10.0],[10.0,30.0],[40.0,40.0]]}"); + private final JsonNode wktJsonNode = + objectMapper.getNodeFactory().textNode("LINESTRING (30 10, 10 30, 40 40)"); + private final JsonNode wkbBase64JsonNode = + objectMapper + .getNodeFactory() + .textNode("AQIAAAADAAAAAAAAAAAAPkAAAAAAAAAkQAAAAAAAACRAAAAAAAAAPkAAAAAAAABEQAAAAAAAAERA"); + private final JsonNode wkbHexJsonNode = + objectMapper + .getNodeFactory() + .textNode( + "0x0102000000030000000000000000003e40000000000000244000000000000024400000000000003e4000000000000044400000000000004440"); + + JsonNodeToLineStringCodecTest() throws IOException {} + + @Test + void should_convert_from_valid_external() throws IOException { + JsonNodeToLineStringCodec codec = + new JsonNodeToLineStringCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("'LINESTRING (30 10, 10 30, 40 40)'")) + .toInternal(lineString) + .convertsFromExternal(JSON_NODE_FACTORY.textNode(" linestring (30 10, 10 30, 40 40) ")) + .toInternal(lineString) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode(objectMapper.writeValueAsString(geoJsonNode))) + .toInternal(lineString) + .convertsFromExternal(geoJsonNode) + .toInternal(lineString) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToLineStringCodec codec = + new JsonNodeToLineStringCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(lineString).toExternal(wktJsonNode); + codec = new JsonNodeToLineStringCodec(objectMapper, JsonGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(lineString).toExternal(geoJsonNode); + codec = + new JsonNodeToLineStringCodec( + objectMapper, WellKnownBinaryGeoFormat.BASE64_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(lineString).toExternal(wkbBase64JsonNode); + codec = + new JsonNodeToLineStringCodec( + objectMapper, WellKnownBinaryGeoFormat.HEX_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(lineString).toExternal(wkbHexJsonNode); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToLineStringCodec codec = + new JsonNodeToLineStringCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid linestring literal")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPointCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPointCodecTest.java new file mode 100644 index 0000000..95c9d08 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPointCodecTest.java @@ -0,0 +1,106 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.geometry.Point; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPoint; +import com.datastax.oss.driver.api.core.data.ByteUtils; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownBinaryGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownTextGeoFormat; +import com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToPointCodecTest { + + private final List nullStrings = Lists.newArrayList("NULL"); + private final Point point = new DefaultPoint(-1.1, -2.2); + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + private final JsonNode geoJsonNode = + objectMapper.readTree("{\"type\":\"Point\",\"coordinates\":[-1.1,-2.2]}"); + private final JsonNode wktJsonNode = objectMapper.getNodeFactory().textNode("POINT (-1.1 -2.2)"); + private final JsonNode wkbJsonNode = + objectMapper.getNodeFactory().binaryNode(ByteUtils.getArray(point.asWellKnownBinary())); + private final JsonNode wkbBase64JsonNode = + objectMapper.getNodeFactory().textNode("AQEAAACamZmZmZnxv5qZmZmZmQHA"); + private final JsonNode wkbHexJsonNode = + objectMapper.getNodeFactory().textNode("0x01010000009a9999999999f1bf9a999999999901c0"); + + JsonNodeToPointCodecTest() throws IOException {} + + @Test + void should_convert_from_valid_external() throws IOException { + JsonNodeToPointCodec codec = + new JsonNodeToPointCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("'POINT (-1.1 -2.2)'")) + .toInternal(point) + .convertsFromExternal(JSON_NODE_FACTORY.textNode(" point (-1.1 -2.2) ")) + .toInternal(point) + .convertsFromExternal(wkbJsonNode) + .toInternal(point) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("AQEAAACamZmZmZnxv5qZmZmZmQHA")) + .toInternal(point) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode("0x01010000009a9999999999f1bf9a999999999901c0")) + .toInternal(point) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode(objectMapper.writeValueAsString(geoJsonNode))) + .toInternal(point) + .convertsFromExternal(geoJsonNode) + .toInternal(point) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToPointCodec codec = + new JsonNodeToPointCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(point).toExternal(wktJsonNode); + codec = new JsonNodeToPointCodec(objectMapper, JsonGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(point).toExternal(geoJsonNode); + codec = + new JsonNodeToPointCodec( + objectMapper, WellKnownBinaryGeoFormat.BASE64_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(point).toExternal(wkbBase64JsonNode); + codec = + new JsonNodeToPointCodec(objectMapper, WellKnownBinaryGeoFormat.HEX_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(point).toExternal(wkbHexJsonNode); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToPointCodec codec = + new JsonNodeToPointCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid point literal")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPolygonCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPolygonCodecTest.java new file mode 100644 index 0000000..03fd34b --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/json/dse/JsonNodeToPolygonCodecTest.java @@ -0,0 +1,128 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.json.dse; + +import static com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils.JSON_NODE_FACTORY; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.geometry.Polygon; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPoint; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPolygon; +import com.datastax.oss.driver.api.core.data.ByteUtils; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownBinaryGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownTextGeoFormat; +import com.datastax.oss.dsbulk.codecs.text.json.JsonCodecUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonNodeToPolygonCodecTest { + + private final List nullStrings = Lists.newArrayList("NULL"); + private final Polygon polygon = + new DefaultPolygon( + new DefaultPoint(30, 10), + new DefaultPoint(10, 20), + new DefaultPoint(20, 40), + new DefaultPoint(40, 40)); + private final ObjectMapper objectMapper = JsonCodecUtils.getObjectMapper(); + private final JsonNode geoJsonNode = + objectMapper.readTree( + "{\"type\":\"Polygon\",\"coordinates\":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]]}"); + private final JsonNode wktJsonNode = + objectMapper.getNodeFactory().textNode("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"); + private final JsonNode wkbJsonNode = + objectMapper.getNodeFactory().binaryNode(ByteUtils.getArray(polygon.asWellKnownBinary())); + private final JsonNode wkbBase64JsonNode = + objectMapper + .getNodeFactory() + .textNode( + "AQMAAAABAAAABQAAAAAAAAAAAD5AAAAAAAAAJEAAAAAAAABEQAAAAAAAAERAAAAAAAAANEAAAAAAAABEQAAAAAAAACRAAAAAAAAANEAAAAAAAAA+QAAAAAAAACRA"); + private final JsonNode wkbHexJsonNode = + objectMapper + .getNodeFactory() + .textNode( + "0x010300000001000000050000000000000000003e4000000000000024400000000000004440000000000000444" + + "000000000000034400000000000004440000000000000244000000000000034400000000000003e400000000000002440"); + + JsonNodeToPolygonCodecTest() throws IOException {} + + @Test + void should_convert_from_valid_external() throws IOException { + JsonNodeToPolygonCodec codec = + new JsonNodeToPolygonCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode("'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))'")) + .toInternal(polygon) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode(" polygon ((30 10, 40 40, 20 40, 10 20, 30 10)) ")) + .toInternal(polygon) + .convertsFromExternal(wkbJsonNode) + .toInternal(polygon) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode( + "AQMAAAABAAAABQAAAAAAAAAAAD5AAAAAAAAAJEAAAAAAAABEQAAAAAAAAERAAAAAAAAANEAAAAAAAABEQAAAAAAAACRAAAAAAAAANEAAAAAAAAA+QAAAAAAAACRA")) + .toInternal(polygon) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode( + "0x010300000001000000050000000000000000003e4000000000000024400000000000004440000000000000444" + + "000000000000034400000000000004440000000000000244000000000000034400000000000003e400000000000002440")) + .toInternal(polygon) + .convertsFromExternal( + JSON_NODE_FACTORY.textNode(objectMapper.writeValueAsString(geoJsonNode))) + .toInternal(polygon) + .convertsFromExternal(geoJsonNode) + .toInternal(polygon) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("NULL")) + .toInternal(null) + .convertsFromExternal(JSON_NODE_FACTORY.textNode("")) + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + JsonNodeToPolygonCodec codec = + new JsonNodeToPolygonCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(polygon).toExternal(wktJsonNode); + codec = new JsonNodeToPolygonCodec(objectMapper, JsonGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(polygon).toExternal(geoJsonNode); + codec = + new JsonNodeToPolygonCodec( + objectMapper, WellKnownBinaryGeoFormat.BASE64_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(polygon).toExternal(wkbBase64JsonNode); + codec = + new JsonNodeToPolygonCodec( + objectMapper, WellKnownBinaryGeoFormat.HEX_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(polygon).toExternal(wkbHexJsonNode); + } + + @Test + void should_not_convert_from_invalid_external() { + JsonNodeToPolygonCodec codec = + new JsonNodeToPolygonCodec(objectMapper, WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .cannotConvertFromExternal(JSON_NODE_FACTORY.textNode("not a valid polygon literal")); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigDecimalCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigDecimalCodecTest.java new file mode 100644 index 0000000..fbdba39 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigDecimalCodecTest.java @@ -0,0 +1,88 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.BigDecimal.ONE; +import static java.math.BigDecimal.ZERO; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToBigDecimalCodecTest { + + StringToBigDecimalCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToBigDecimalCodec) + codecFactory.createConvertingCodec( + DataTypes.DECIMAL, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal(ZERO) + .convertsFromExternal("-1234.56") + .toInternal(new BigDecimal("-1234.56")) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(new BigDecimal("0")) + .convertsFromExternal("2000-01-01T00:00:00Z") + .toInternal(new BigDecimal("946684800000")) + .convertsFromExternal("true") + .toInternal(new BigDecimal("1")) + .convertsFromExternal("false") + .toInternal(new BigDecimal("0")) + .convertsFromExternal("TRUE") + .toInternal(ONE) + .convertsFromExternal("FALSE") + .toInternal(ZERO) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(ZERO) + .toExternal("0") + .convertsFromInternal(new BigDecimal("1234.56")) + .toExternal("1,234.56") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal("not a valid decimal"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigIntegerCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigIntegerCodecTest.java new file mode 100644 index 0000000..c6fab59 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBigIntegerCodecTest.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.math.BigInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToBigIntegerCodecTest { + + private StringToBigIntegerCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToBigIntegerCodec) + codecFactory.createConvertingCodec( + DataTypes.VARINT, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal(BigInteger.ZERO) + .convertsFromExternal("-1,234") + .toInternal(new BigInteger("-1234")) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(new BigInteger("0")) + .convertsFromExternal("2000-01-01T00:00:00Z") + .toInternal(new BigInteger("946684800000")) + .convertsFromExternal("TRUE") + .toInternal(BigInteger.ONE) + .convertsFromExternal("FALSE") + .toInternal(BigInteger.ZERO) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(BigInteger.ZERO) + .toExternal("0") + .convertsFromInternal(new BigInteger("-1234")) + .toExternal("-1,234") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal("-1.234") + .cannotConvertFromExternal("not a valid biginteger"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBlobCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBlobCodecTest.java new file mode 100644 index 0000000..0426dde --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBlobCodecTest.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.binary.Base64BinaryFormat; +import com.datastax.oss.dsbulk.codecs.api.format.binary.HexBinaryFormat; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.Base64; +import org.junit.jupiter.api.Test; + +class StringToBlobCodecTest { + + private final byte[] data = {1, 2, 3, 4, 5, 6}; + private final byte[] empty = {}; + + private final ByteBuffer dataBb = ByteBuffer.wrap(data); + private final ByteBuffer emptyBb = ByteBuffer.wrap(empty); + + private final String data64 = Base64.getEncoder().encodeToString(data); + private final String dataHex = Bytes.toHexString(data); + + private final StringToBlobCodec codec64 = + new StringToBlobCodec(Lists.newArrayList("NULL"), Base64BinaryFormat.INSTANCE); + private final StringToBlobCodec codecHex = + new StringToBlobCodec(Lists.newArrayList("NULL"), HexBinaryFormat.INSTANCE); + + @Test + void should_convert_from_valid_external_base64() { + assertThat(codec64) + .convertsFromExternal(data64) + .toInternal(dataBb) + .convertsFromExternal(dataHex) + .toInternal(dataBb) + .convertsFromExternal("0x") + .toInternal(emptyBb) + .convertsFromExternal("") + // DAT-573: consider empty string as empty byte array + .toInternal(emptyBb) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_external_hex() { + assertThat(codecHex) + .convertsFromExternal(data64) + .toInternal(dataBb) + .convertsFromExternal(dataHex) + .toInternal(dataBb) + .convertsFromExternal("0x") + .toInternal(emptyBb) + .convertsFromExternal("") + // DAT-573: consider empty string as empty byte array + .toInternal(emptyBb) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal_base64() { + assertThat(codec64) + .convertsFromInternal(dataBb) + .toExternal(data64) + .convertsFromInternal(emptyBb) + .toExternal("") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_convert_from_valid_internal_hex() { + assertThat(codecHex) + .convertsFromInternal(dataBb) + .toExternal(dataHex) + .convertsFromInternal(emptyBb) + .toExternal("0x") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec64).cannotConvertFromExternal("not a valid binary"); + assertThat(codecHex).cannotConvertFromExternal("not a valid binary"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBooleanCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBooleanCodecTest.java new file mode 100644 index 0000000..111c1c0 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToBooleanCodecTest.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class StringToBooleanCodecTest { + + private final Map inputs = + ImmutableMap.builder().put("foo", true).put("bar", false).build(); + + private final Map outputs = + ImmutableMap.builder().put(true, "foo").put(false, "bar").build(); + + private final StringToBooleanCodec codec = + new StringToBooleanCodec(inputs, outputs, Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("FOO") + .toInternal(true) + .convertsFromExternal("BAR") + .toInternal(false) + .convertsFromExternal("foo") + .toInternal(true) + .convertsFromExternal("bar") + .toInternal(false) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(true) + .toExternal("foo") + .convertsFromInternal(false) + .toExternal("bar") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal("not a valid boolean"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToByteCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToByteCodecTest.java new file mode 100644 index 0000000..d285a6a --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToByteCodecTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToByteCodecTest { + + private StringToByteCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToByteCodec) + codecFactory.createConvertingCodec( + DataTypes.TINYINT, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal((byte) 0) + .convertsFromExternal("127") + .toInternal((byte) 127) + .convertsFromExternal("-128") + .toInternal((byte) -128) + .convertsFromExternal("0") + .toInternal((byte) 0) + .convertsFromExternal("127") + .toInternal((byte) 127) + .convertsFromExternal("-128") + .toInternal((byte) -128) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal((byte) 0) + .convertsFromExternal("TRUE") + .toInternal((byte) 1) + .convertsFromExternal("FALSE") + .toInternal((byte) 0) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal((byte) 0) + .toExternal("0") + .convertsFromInternal((byte) 127) + .toExternal("127") + .convertsFromInternal((byte) -128) + .toExternal("-128") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal("not a valid byte") + .cannotConvertFromExternal("1.2") + .cannotConvertFromExternal("128") + .cannotConvertFromExternal("-129") + .cannotConvertFromExternal("2000-01-01T00:00:00Z") // overflow + ; + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDoubleCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDoubleCodecTest.java new file mode 100644 index 0000000..6133381 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDoubleCodecTest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToDoubleCodecTest { + + private StringToDoubleCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext() + .setNullStrings("NULL") + .setFormatNumbers(true) + .setRoundingMode(HALF_EVEN); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToDoubleCodec) + codecFactory.createConvertingCodec( + DataTypes.DOUBLE, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal(0d) + .convertsFromExternal("1234.56") + .toInternal(1234.56d) + .convertsFromExternal("1,234.56") + .toInternal(1234.56d) + .convertsFromExternal("1.7976931348623157E308") + .toInternal(Double.MAX_VALUE) + .convertsFromExternal("4.9E-324") + .toInternal(Double.MIN_VALUE) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(0d) + .convertsFromExternal("2000-01-01T00:00:00Z") + .toInternal(946684800000d) + .convertsFromExternal("TRUE") + .toInternal(1d) + .convertsFromExternal("FALSE") + .toInternal(0d) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0d) + .toExternal("0") + .convertsFromInternal(1234.56d) + .toExternal("1,234.56") + .convertsFromInternal(0.001) + .toExternal("0") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal("not a valid double"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDurationCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDurationCodecTest.java new file mode 100644 index 0000000..d4e08d0 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToDurationCodecTest.java @@ -0,0 +1,64 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import org.junit.jupiter.api.Test; + +class StringToDurationCodecTest { + + private final long nanosPerMinute = 60 * 1000L * 1000L * 1000L; + + private final CqlDuration duration = CqlDuration.newInstance(15, 0, 130 * nanosPerMinute); + + private final StringToDurationCodec codec = new StringToDurationCodec(Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("1y3mo2h10m") // standard pattern + .toInternal(duration) + .convertsFromExternal("P1Y3MT2H10M") // ISO 8601 pattern + .toInternal(duration) + .convertsFromExternal("P0001-03-00T02:10:00") // alternative ISO 8601 pattern + .toInternal(duration) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(duration) + .toExternal("1y3mo2h10m") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal("1Y3M4D") // The minutes should be after days + .cannotConvertFromExternal("not a valid duration"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToFloatCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToFloatCodecTest.java new file mode 100644 index 0000000..17d84c6 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToFloatCodecTest.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToFloatCodecTest { + + private StringToFloatCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext() + .setNullStrings("NULL") + .setFormatNumbers(true) + .setRoundingMode(HALF_EVEN); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToFloatCodec) + codecFactory.createConvertingCodec( + DataTypes.FLOAT, GenericType.STRING, true); + } + + @Test + @SuppressWarnings("FloatingPointLiteralPrecision") + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal(0f) + .convertsFromExternal("1234.56") + .toInternal(1234.56f) + .convertsFromExternal("1,234.56") + .toInternal(1234.56f) + .convertsFromExternal("3.4028235E38") + .toInternal(Float.MAX_VALUE) + .convertsFromExternal("1.4E-45") + .toInternal(Float.MIN_VALUE) + .convertsFromExternal("340,282,350,000,000,000,000,000,000,000,000,000,000") + .toInternal(Float.MAX_VALUE) + .convertsFromExternal("0.0000000000000000000000000000000000000000000014") + .toInternal(Float.MIN_VALUE) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(0f) + .convertsFromExternal("TRUE") + .toInternal(1f) + .convertsFromExternal("FALSE") + .toInternal(0f) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0f) + .toExternal("0") + .convertsFromInternal(1234.56f) + .toExternal("1,234.56") + .convertsFromInternal(Float.MAX_VALUE) + .toExternal("340,282,350,000,000,000,000,000,000,000,000,000,000") + .convertsFromInternal(0.001f) + .toExternal("0") // decimals truncated + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal("not a valid float"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInetAddressCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInetAddressCodecTest.java new file mode 100644 index 0000000..6f058ea --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInetAddressCodecTest.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.net.InetAddress; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class StringToInetAddressCodecTest { + + private StringToInetAddressCodec codec = new StringToInetAddressCodec(Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() throws Exception { + assertThat(codec) + .convertsFromExternal("1.2.3.4") + .toInternal(InetAddress.getByName("1.2.3.4")) + .convertsFromExternal("127.0.0.1") + .toInternal(InetAddress.getByName("127.0.0.1")) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() throws Exception { + assertThat(codec) + .convertsFromInternal(InetAddress.getByName("1.2.3.4")) + .toExternal("1.2.3.4") + .convertsFromInternal(InetAddress.getByName("127.0.0.1")) + .toExternal("127.0.0.1") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + @Disabled + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal("not a valid inet address"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInstantCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInstantCodecTest.java new file mode 100644 index 0000000..8d90f37 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToInstantCodecTest.java @@ -0,0 +1,143 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.util.concurrent.TimeUnit.MINUTES; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.CqlTemporalFormat; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToInstantCodecTest { + + private final Instant millennium = Instant.parse("2000-01-01T00:00:00Z"); + private final Instant minutesAfterMillennium = millennium.plus(Duration.ofMinutes(123456)); + + private StringToInstantCodec codec1; + private StringToInstantCodec codec2; + private StringToInstantCodec codec3; + private StringToInstantCodec codec4; + + @BeforeEach + void setUpCodec1() { + ConversionContext context1 = new TextConversionContext().setNullStrings("NULL"); + ConversionContext context2 = + new TextConversionContext().setNullStrings("NULL").setTimestampFormat("yyyyMMddHHmmss"); + ConversionContext context3 = + new TextConversionContext() + .setNullStrings("NULL") + .setTimeUnit(MINUTES) + .setEpoch(ZonedDateTime.parse("2000-01-01T00:00:00Z")); + ConversionContext context4 = + new TextConversionContext() + .setNullStrings("NULL") + .setTimeUnit(MINUTES) + .setEpoch(ZonedDateTime.parse("2000-01-01T00:00:00Z")) + .setTimestampFormat("UNITS_SINCE_EPOCH"); + codec1 = + (StringToInstantCodec) + new ConvertingCodecFactory(context1) + .createConvertingCodec( + DataTypes.TIMESTAMP, GenericType.STRING, true); + codec2 = + (StringToInstantCodec) + new ConvertingCodecFactory(context2) + .createConvertingCodec( + DataTypes.TIMESTAMP, GenericType.STRING, true); + codec3 = + (StringToInstantCodec) + new ConvertingCodecFactory(context3) + .createConvertingCodec( + DataTypes.TIMESTAMP, GenericType.STRING, true); + codec4 = + (StringToInstantCodec) + new ConvertingCodecFactory(context4) + .createConvertingCodec( + DataTypes.TIMESTAMP, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec1) + .convertsFromExternal("2016-07-24T20:34") + .toInternal(Instant.parse("2016-07-24T20:34:00Z")) + .convertsFromExternal("2016-07-24T20:34:12") + .toInternal(Instant.parse("2016-07-24T20:34:12Z")) + .convertsFromExternal("2016-07-24T20:34:12.999") + .toInternal(Instant.parse("2016-07-24T20:34:12.999Z")) + .convertsFromExternal("2016-07-24T20:34+01:00") + .toInternal(Instant.parse("2016-07-24T19:34:00Z")) + .convertsFromExternal("2016-07-24T20:34:12.999+01:00") + .toInternal(Instant.parse("2016-07-24T19:34:12.999Z")) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + assertThat(codec2) + .convertsFromExternal("20160724203412") + .toInternal(Instant.parse("2016-07-24T20:34:12Z")) + .convertsFromExternal("NULL") + .toInternal(null); + assertThat(codec3) + .convertsFromExternal(CqlTemporalFormat.DEFAULT_INSTANCE.format(minutesAfterMillennium)) + .toInternal(minutesAfterMillennium); + assertThat(codec4).convertsFromExternal("123456").toInternal(minutesAfterMillennium); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec1) + .convertsFromInternal(Instant.parse("2016-07-24T20:34:00Z")) + .toExternal("2016-07-24T20:34:00Z") + .convertsFromInternal(Instant.parse("2016-07-24T20:34:12Z")) + .toExternal("2016-07-24T20:34:12Z") + .convertsFromInternal(Instant.parse("2016-07-24T20:34:12.999Z")) + .toExternal("2016-07-24T20:34:12.999Z") + .convertsFromInternal(Instant.parse("2016-07-24T19:34:00Z")) + .toExternal("2016-07-24T19:34:00Z") + .convertsFromInternal(Instant.parse("2016-07-24T19:34:12.999Z")) + .toExternal("2016-07-24T19:34:12.999Z") + .convertsFromInternal(null) + .toExternal("NULL"); + assertThat(codec2) + .convertsFromInternal(Instant.parse("2016-07-24T20:34:12Z")) + .toExternal("20160724203412") + .convertsFromInternal(null) + .toExternal("NULL"); + // conversion back to numeric timestamps is not possible, values are always formatted with full + // alphanumeric pattern + assertThat(codec3) + .convertsFromInternal(minutesAfterMillennium) + .toExternal(CqlTemporalFormat.DEFAULT_INSTANCE.format(minutesAfterMillennium)); + assertThat(codec4).convertsFromInternal(minutesAfterMillennium).toExternal("123456"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec1).cannotConvertFromExternal("not a valid date format"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToIntegerCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToIntegerCodecTest.java new file mode 100644 index 0000000..2528ddc --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToIntegerCodecTest.java @@ -0,0 +1,159 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToIntegerCodecTest { + + private StringToIntegerCodec codec1; + private StringToIntegerCodec codec2; + + @BeforeEach + void setUpCodec1() { + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(new TextConversionContext()); + codec1 = + (StringToIntegerCodec) + codecFactory.createConvertingCodec( + DataTypes.INT, GenericType.STRING, true); + } + + @BeforeEach + void setUpCodec2() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL", "NADA").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec2 = + (StringToIntegerCodec) + codecFactory.createConvertingCodec( + DataTypes.INT, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec1) + .convertsFromExternal("0") + .toInternal(0) + .convertsFromExternal("2147483647") + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal("-2147483648") + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal("2,147,483,647") + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal("-2,147,483,648") + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal("2,147,483,647") + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal("-2,147,483,648") + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(0) + .convertsFromExternal("TRUE") + .toInternal(1) + .convertsFromExternal("FALSE") + .toInternal(0) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_external_with_custom_settings() { + assertThat(codec2) + .convertsFromExternal("0") + .toInternal(0) + .convertsFromExternal("2147483647") + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal("-2147483648") + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal("2,147,483,647") + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal("-2,147,483,648") + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal("2,147,483,647") + .toInternal(Integer.MAX_VALUE) + .convertsFromExternal("-2,147,483,648") + .toInternal(Integer.MIN_VALUE) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(0) + .convertsFromExternal("TRUE") + .toInternal(1) + .convertsFromExternal("FALSE") + .toInternal(0) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("NADA") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec1) + .convertsFromInternal(0) + .toExternal("0") + .convertsFromInternal(Integer.MAX_VALUE) + .toExternal("2147483647") + .convertsFromInternal(Integer.MIN_VALUE) + .toExternal("-2147483648") + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_convert_from_valid_internal_with_custom_settings() { + assertThat(codec2) + .convertsFromInternal(0) + .toExternal("0") + .convertsFromInternal(Integer.MAX_VALUE) + .toExternal("2,147,483,647") + .convertsFromInternal(Integer.MIN_VALUE) + .toExternal("-2,147,483,648") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec1) + .cannotConvertFromExternal("NULL") + .cannotConvertFromExternal("not a valid integer") + .cannotConvertFromExternal("1.2") + .cannotConvertFromExternal("2147483648") + .cannotConvertFromExternal("-2147483649") + .cannotConvertFromExternal("2000-01-01T00:00:00Z") // overflow + ; + assertThat(codec2) + .cannotConvertFromExternal("not a valid integer") + .cannotConvertFromExternal("1.2") + .cannotConvertFromExternal("2147483648") + .cannotConvertFromExternal("-2147483649") + .cannotConvertFromExternal("2000-01-01T00:00:00Z") // overflow + ; + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToListCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToListCodecTest.java new file mode 100644 index 0000000..d21e7fc --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToListCodecTest.java @@ -0,0 +1,181 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToListCodecTest { + + private StringToListCodec codec1; + private StringToListCodec codec2; + private StringToListCodec codec3; + + private Instant i1 = Instant.parse("2016-07-24T20:34:12.999Z"); + private Instant i2 = Instant.parse("2018-05-25T18:34:12.999Z"); + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec1 = + (StringToListCodec) + codecFactory.>createConvertingCodec( + DataTypes.listOf(DataTypes.DOUBLE), GenericType.STRING, true); + codec2 = + (StringToListCodec) + codecFactory.>createConvertingCodec( + DataTypes.listOf(DataTypes.TIMESTAMP), GenericType.STRING, true); + codec3 = + (StringToListCodec) + codecFactory.>createConvertingCodec( + DataTypes.listOf(DataTypes.TEXT), GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec1) + .convertsFromExternal("[1,2,3]") + .toInternal(Lists.newArrayList(1d, 2d, 3d)) + .convertsFromExternal("1,2,3") + .toInternal(Lists.newArrayList(1d, 2d, 3d)) + .convertsFromExternal(" [ 1 , 2 , 3 ] ") + .toInternal(Lists.newArrayList(1d, 2d, 3d)) + .convertsFromExternal("[1234.56,78900]") + .toInternal(Lists.newArrayList(1234.56d, 78900d)) + .convertsFromExternal("[\"1,234.56\",\"78,900\"]") + .toInternal(Lists.newArrayList(1234.56d, 78900d)) + .convertsFromExternal("[,]") + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("[]") + .toInternal(Lists.newArrayList()) + .convertsFromExternal("") + .toInternal(null); + assertThat(codec2) + .convertsFromExternal("[\"2016-07-24T20:34:12.999Z\",\"2018-05-25 20:34:12.999+02:00\"]") + .toInternal(Lists.newArrayList(i1, i2)) + .convertsFromExternal("\"2016-07-24T20:34:12.999Z\",\"2018-05-25 20:34:12.999+02:00\"") + .toInternal(Lists.newArrayList(i1, i2)) + .convertsFromExternal("['2016-07-24T20:34:12.999Z','2018-05-25 20:34:12.999+02:00']") + .toInternal(Lists.newArrayList(i1, i2)) + .convertsFromExternal( + " [ \"2016-07-24T20:34:12.999Z\" , \"2018-05-25 20:34:12.999+02:00\" ] ") + .toInternal(Lists.newArrayList(i1, i2)) + .convertsFromExternal("[,]") + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal("[null,null]") + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("[]") + .toInternal(Lists.newArrayList()) + .convertsFromExternal("") + .toInternal(null); + assertThat(codec3) + .convertsFromExternal("[\"foo\",\"bar\"]") + .toInternal(Lists.newArrayList("foo", "bar")) + .convertsFromExternal("['foo','bar']") + .toInternal(Lists.newArrayList("foo", "bar")) + .convertsFromExternal(" [ \"foo\" , \"bar\" ] ") + .toInternal(Lists.newArrayList("foo", "bar")) + .convertsFromExternal("[,]") + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal("[null,null]") + .toInternal(Lists.newArrayList(null, null)) + .convertsFromExternal("['','']") + .toInternal(Lists.newArrayList("", "")) + .convertsFromExternal("[\"\",\"\"]") + .toInternal(Lists.newArrayList("", "")) + .convertsFromExternal("[\"NULL\",\"NULL\"]") + .toInternal(Lists.newArrayList("NULL", "NULL")) + .convertsFromExternal("['NULL','NULL']") + .toInternal(Lists.newArrayList("NULL", "NULL")) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("[]") + .toInternal(Lists.newArrayList()) + .convertsFromExternal("") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec1) + .convertsFromInternal(Lists.newArrayList(1d, 2d, 3d)) + .toExternal("[1.0,2.0,3.0]") + .convertsFromInternal(Lists.newArrayList(1234.56d, 78900d)) + .toExternal("[1234.56,78900.0]") + .convertsFromInternal(Lists.newArrayList(1d, null)) + .toExternal("[1.0,null]") + .convertsFromInternal(Lists.newArrayList(null, 0d)) + .toExternal("[null,0.0]") + .convertsFromInternal(Lists.newArrayList(null, null)) + .toExternal("[null,null]") + .convertsFromInternal(Lists.newArrayList()) + .toExternal("[]") + .convertsFromInternal(null) + .toExternal("NULL"); + assertThat(codec2) + .convertsFromInternal(Lists.newArrayList(i1, i2)) + .toExternal("[\"2016-07-24T20:34:12.999Z\",\"2018-05-25T18:34:12.999Z\"]") + .convertsFromInternal(Lists.newArrayList(null, null)) + .toExternal("[null,null]") + .convertsFromInternal(Lists.newArrayList()) + .toExternal("[]") + .convertsFromInternal(null) + .toExternal("NULL"); + assertThat(codec3) + .convertsFromInternal(Lists.newArrayList("foo", "bar")) + .toExternal("[\"foo\",\"bar\"]") + .convertsFromInternal(Lists.newArrayList("", "")) + .toExternal("[\"\",\"\"]") + .convertsFromInternal(Lists.newArrayList(null, null)) + .toExternal("[null,null]") + .convertsFromInternal(Lists.newArrayList()) + .toExternal("[]") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec1).cannotConvertFromExternal("[1,\"not a valid double\"]"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalDateCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalDateCodecTest.java new file mode 100644 index 0000000..1c2ff8a --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalDateCodecTest.java @@ -0,0 +1,102 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; + +class StringToLocalDateCodecTest { + + private TemporalFormat format1 = + CodecUtils.getTemporalFormat( + "ISO_LOCAL_DATE", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format2 = + CodecUtils.getTemporalFormat( + "yyyyMMdd", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format3 = + CodecUtils.getTemporalFormat( + "UNITS_SINCE_EPOCH", + UTC, + US, + DAYS, + ZonedDateTime.parse("2000-01-01T00:00:00Z"), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private final List nullStrings = Lists.newArrayList("NULL"); + + @Test + void should_convert_from_valid_external() { + StringToLocalDateCodec codec = new StringToLocalDateCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal("2016-07-24") + .toInternal(LocalDate.parse("2016-07-24")) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + codec = new StringToLocalDateCodec(format2, UTC, nullStrings); + assertThat(codec).convertsFromExternal("20160724").toInternal(LocalDate.parse("2016-07-24")); + codec = new StringToLocalDateCodec(format3, UTC, nullStrings); + // 12 full days after year 2000 = 2000-01-13 (at midnight) + assertThat(codec).convertsFromExternal("12").toInternal(LocalDate.parse("2000-01-13")); + } + + @Test + void should_convert_from_valid_internal() { + StringToLocalDateCodec codec = new StringToLocalDateCodec(format1, UTC, nullStrings); + assertThat(codec).convertsFromInternal(LocalDate.parse("2016-07-24")).toExternal("2016-07-24"); + codec = new StringToLocalDateCodec(format2, UTC, nullStrings); + assertThat(codec).convertsFromInternal(LocalDate.parse("2016-07-24")).toExternal("20160724"); + codec = new StringToLocalDateCodec(format3, UTC, nullStrings); + assertThat(codec).convertsFromInternal(LocalDate.parse("2000-01-13")).toExternal("12"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToLocalDateCodec codec = new StringToLocalDateCodec(format1, UTC, nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid date format"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalTimeCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalTimeCodecTest.java new file mode 100644 index 0000000..75835a3 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLocalTimeCodecTest.java @@ -0,0 +1,110 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; + +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.temporal.TemporalFormat; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; + +class StringToLocalTimeCodecTest { + + private TemporalFormat format1 = + CodecUtils.getTemporalFormat( + "ISO_LOCAL_TIME", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format2 = + CodecUtils.getTemporalFormat( + "HHmmss.SSS", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private TemporalFormat format3 = + CodecUtils.getTemporalFormat( + "UNITS_SINCE_EPOCH", + UTC, + US, + MINUTES, + ZonedDateTime.parse("2000-01-01T00:00:00Z"), + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true), + false); + + private final List nullStrings = Lists.newArrayList("NULL"); + + @Test + void should_convert_from_valid_external() { + StringToLocalTimeCodec codec = new StringToLocalTimeCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal("12:24:46") + .toInternal(LocalTime.parse("12:24:46")) + .convertsFromExternal("12:24:46.999") + .toInternal(LocalTime.parse("12:24:46.999")) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + codec = new StringToLocalTimeCodec(format2, UTC, nullStrings); + assertThat(codec) + .convertsFromExternal("122446.999") + .toInternal(LocalTime.parse("12:24:46.999")); + codec = new StringToLocalTimeCodec(format3, UTC, nullStrings); + // 123 minutes after year 2000 = 02:03:00 + assertThat(codec).convertsFromExternal("123").toInternal(LocalTime.parse("02:03:00")); + } + + @Test + void should_convert_from_valid_internal() { + StringToLocalTimeCodec codec = new StringToLocalTimeCodec(format1, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalTime.parse("12:24:46.999")) + .toExternal("12:24:46.999"); + codec = new StringToLocalTimeCodec(format2, UTC, nullStrings); + assertThat(codec) + .convertsFromInternal(LocalTime.parse("12:24:46.999")) + .toExternal("122446.999"); + codec = new StringToLocalTimeCodec(format3, UTC, nullStrings); + assertThat(codec).convertsFromInternal(LocalTime.parse("02:03:00")).toExternal("123"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToLocalTimeCodec codec = new StringToLocalTimeCodec(format1, UTC, nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid date format"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLongCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLongCodecTest.java new file mode 100644 index 0000000..bde8aa1 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToLongCodecTest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToLongCodecTest { + + private StringToLongCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToLongCodec) + codecFactory.createConvertingCodec( + DataTypes.BIGINT, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal(0L) + .convertsFromExternal("9223372036854775807") + .toInternal(Long.MAX_VALUE) + .convertsFromExternal("-9223372036854775808") + .toInternal(Long.MIN_VALUE) + .convertsFromExternal("9,223,372,036,854,775,807") + .toInternal(Long.MAX_VALUE) + .convertsFromExternal("-9,223,372,036,854,775,808") + .toInternal(Long.MIN_VALUE) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal(0L) + .convertsFromExternal("2000-01-01T00:00:00Z") + .toInternal(946684800000L) + .convertsFromExternal("TRUE") + .toInternal(1L) + .convertsFromExternal("FALSE") + .toInternal(0L) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(0L) + .toExternal("0") + .convertsFromInternal(Long.MAX_VALUE) + .toExternal("9,223,372,036,854,775,807") + .convertsFromInternal(Long.MIN_VALUE) + .toExternal("-9,223,372,036,854,775,808") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal("not a valid long") + .cannotConvertFromExternal("1.2") + .cannotConvertFromExternal("9223372036854775808") + .cannotConvertFromExternal("-9223372036854775809"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToMapCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToMapCodecTest.java new file mode 100644 index 0000000..c2ddb0e --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToMapCodecTest.java @@ -0,0 +1,116 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToMapCodecTest { + + private StringToMapCodec> codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToMapCodec>) + codecFactory.>>createConvertingCodec( + DataTypes.mapOf(DataTypes.DOUBLE, DataTypes.listOf(DataTypes.TEXT)), + GenericType.STRING, + true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("{1 : [\"foo\", \"bar\"], 2:[\"qix\"]}") + .toInternal(map(1d, list("foo", "bar"), 2d, list("qix"))) + .convertsFromExternal("1 : [\"foo\", \"bar\"], 2:[\"qix\"]") + .toInternal(map(1d, list("foo", "bar"), 2d, list("qix"))) + .convertsFromExternal("{ '1234.56' : ['foo', 'bar'], '0.12' : ['qix'] }") + .toInternal(map(1234.56d, list("foo", "bar"), 0.12d, list("qix"))) + .convertsFromExternal("{ '1,234.56' : ['foo'] , '.12' : ['bar']}") + .toInternal(map(1234.56d, list("foo"), 0.12d, list("bar"))) + .convertsFromExternal("{1: [], 2 :[]}") + .toInternal(map(1d, list(), 2d, list())) + // DAT-297: don't apply nullStrings to inner elements + .convertsFromExternal("{1: [\"NULL\"], 2: ['NULL']}") + .toInternal(map(1d, list("NULL"), 2d, list("NULL"))) + .convertsFromExternal("{1: [\"\"], 2: ['']}") + .toInternal(map(1d, list(""), 2d, list(""))) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("{}") + .toInternal(ImmutableMap.of()) + .convertsFromExternal("") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(map(1d, list("foo", "bar"), 2d, list("qix"))) + .toExternal("{\"1\":[\"foo\",\"bar\"],\"2\":[\"qix\"]}") + .convertsFromInternal(map(1234.56d, list("foo", "bar"), 0.12d, list("qix"))) + .toExternal("{\"1,234.56\":[\"foo\",\"bar\"],\"0.12\":[\"qix\"]}") + .convertsFromInternal(map(1d, list(""), 2d, list(""))) + .toExternal("{\"1\":[\"\"],\"2\":[\"\"]}") + .convertsFromInternal(map(1d, null, 2d, list())) + .toExternal("{\"1\":null,\"2\":[]}") + .convertsFromInternal(ImmutableMap.of()) + .toExternal("{}") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal("{\"not a valid input\":\"foo\"}") + .cannotConvertFromExternal("[1,\"not a valid object\"]") + .cannotConvertFromExternal("42"); + } + + private static Map> map( + Double k1, List v1, Double k2, List v2) { + Map> map = new LinkedHashMap<>(); + map.put(k1, v1); + map.put(k2, v2); + return map; + } + + private static List list(String... elements) { + return Arrays.asList(elements); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToSetCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToSetCodecTest.java new file mode 100644 index 0000000..e8676f7 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToSetCodecTest.java @@ -0,0 +1,153 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.util.Set; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToSetCodecTest { + + private StringToSetCodec codec1; + private StringToSetCodec codec2; + + @BeforeEach + void setUp() { + ConversionContext context = new TextConversionContext().setNullStrings("NULL"); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec1 = + (StringToSetCodec) + codecFactory.>createConvertingCodec( + DataTypes.setOf(DataTypes.DOUBLE), GenericType.STRING, true); + codec2 = + (StringToSetCodec) + codecFactory.>createConvertingCodec( + DataTypes.setOf(DataTypes.TEXT), GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec1) + .convertsFromExternal("[1,2,3]") + .toInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .convertsFromExternal("1,2,3") + .toInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .convertsFromExternal(" [ 1 , 2 , 3 ] ") + .toInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .convertsFromExternal("[1234.56,78900]") + .toInternal(Sets.newLinkedHashSet(1234.56d, 78900d)) + .convertsFromExternal("[\"1,234.56\",\"78,900\"]") + .toInternal(Sets.newLinkedHashSet(1234.56d, 78900d)) + .convertsFromExternal("[,]") + .toInternal(Sets.newLinkedHashSet(null, null)) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("[]") + .toInternal(Sets.newLinkedHashSet()) + .convertsFromExternal("") + .toInternal(null); + assertThat(codec2) + .convertsFromExternal("[\"foo\",\"bar\"]") + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal("\"foo\",\"bar\"") + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal("['foo','bar']") + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal(" [ \"foo\" , \"bar\" ] ") + .toInternal(Sets.newLinkedHashSet("foo", "bar")) + .convertsFromExternal("[ \"\\\"foo\\\"\" , \"\\\"bar\\\"\" ]") + .toInternal(Sets.newLinkedHashSet("\"foo\"", "\"bar\"")) + .convertsFromExternal("[ \"\\\"fo\\\\o\\\"\" , \"\\\"ba\\\\r\\\"\" ]") + .toInternal(Sets.newLinkedHashSet("\"fo\\o\"", "\"ba\\r\"")) + .convertsFromExternal("[,]") + .toInternal(Sets.newLinkedHashSet(null, null)) + .convertsFromExternal("[null,null]") + .toInternal(Sets.newLinkedHashSet(null, null)) + // DAT-297: don't apply nullStrings to inner elements + .convertsFromExternal("[\"\",\"\"]") + .toInternal(Sets.newLinkedHashSet("")) + .convertsFromExternal("['','']") + .toInternal(Sets.newLinkedHashSet("")) + .convertsFromExternal("[\"NULL\",\"NULL\"]") + .toInternal(Sets.newLinkedHashSet("NULL")) + .convertsFromExternal("['NULL','NULL']") + .toInternal(Sets.newLinkedHashSet("NULL")) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("[]") + .toInternal(Sets.newLinkedHashSet()) + .convertsFromExternal("") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec1) + .convertsFromInternal(Sets.newLinkedHashSet(1d, 2d, 3d)) + .toExternal("[1.0,2.0,3.0]") + .convertsFromInternal(Sets.newLinkedHashSet(1234.56d, 78900d)) + .toExternal("[1234.56,78900.0]") + .convertsFromInternal(Sets.newLinkedHashSet(1d, null)) + .toExternal("[1.0,null]") + .convertsFromInternal(Sets.newLinkedHashSet(null, 0d)) + .toExternal("[null,0.0]") + .convertsFromInternal(Sets.newLinkedHashSet((Double) null)) + .toExternal("[null]") + .convertsFromInternal(Sets.newLinkedHashSet()) + .toExternal("[]") + .convertsFromInternal(null) + .toExternal("NULL"); + assertThat(codec2) + .convertsFromInternal(Sets.newLinkedHashSet("foo", "bar")) + .toExternal("[\"foo\",\"bar\"]") + .convertsFromInternal(Sets.newLinkedHashSet("\"foo\"", "\"bar\"")) + .toExternal("[\"\\\"foo\\\"\",\"\\\"bar\\\"\"]") + .convertsFromInternal(Sets.newLinkedHashSet("\\foo\\", "\\bar\\")) + .toExternal("[\"\\\\foo\\\\\",\"\\\\bar\\\\\"]") + .convertsFromInternal(Sets.newLinkedHashSet(",foo,", ",bar,")) + .toExternal("[\",foo,\",\",bar,\"]") + .convertsFromInternal(Sets.newLinkedHashSet("")) + .toExternal("[\"\"]") + .convertsFromInternal(Sets.newLinkedHashSet((String) null)) + .toExternal("[null]") + .convertsFromInternal(Sets.newLinkedHashSet()) + .toExternal("[]") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec1) + .cannotConvertFromExternal("[1,\"not a valid double\"]") + .cannotConvertFromExternal("[ \"not a valid array\" : 42 ") + .cannotConvertFromExternal("[42"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToShortCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToShortCodecTest.java new file mode 100644 index 0000000..871e320 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToShortCodecTest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToShortCodecTest { + + private StringToShortCodec codec; + + @BeforeEach + void setUp() { + ConversionContext context = + new TextConversionContext().setNullStrings("NULL").setFormatNumbers(true); + ConvertingCodecFactory codecFactory = new ConvertingCodecFactory(context); + codec = + (StringToShortCodec) + codecFactory.createConvertingCodec( + DataTypes.SMALLINT, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("0") + .toInternal((short) 0) + .convertsFromExternal("32767") + .toInternal((short) 32767) + .convertsFromExternal("-32768") + .toInternal((short) -32768) + .convertsFromExternal("32,767") + .toInternal((short) 32767) + .convertsFromExternal("-32,768") + .toInternal((short) -32768) + .convertsFromExternal("1970-01-01T00:00:00Z") + .toInternal((short) 0) + .convertsFromExternal("TRUE") + .toInternal((short) 1) + .convertsFromExternal("FALSE") + .toInternal((short) 0) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal((short) 0) + .toExternal("0") + .convertsFromInternal((short) 32767) + .toExternal("32,767") + .convertsFromInternal((short) -32768) + .toExternal("-32,768") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec) + .cannotConvertFromExternal("not a valid short") + .cannotConvertFromExternal("1.2") + .cannotConvertFromExternal("32768") + .cannotConvertFromExternal("-32769") + .cannotConvertFromExternal("2000-01-01T00:00:00Z") // overflow + ; + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToStringCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToStringCodecTest.java new file mode 100644 index 0000000..3b45f2e --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToStringCodecTest.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class StringToStringCodecTest { + + @Test + void should_convert_from_valid_external() { + StringToStringCodec codec = new StringToStringCodec(TypeCodecs.TEXT, Collections.emptyList()); + assertThat(codec) + .convertsFromExternal("foo") + .toInternal("foo") + .convertsFromExternal("") + .toInternal("") + .convertsFromExternal(null) + .toInternal(null); + } + + @Test + void should_convert_from_valid_external_with_custom_null_string() { + StringToStringCodec codec = + new StringToStringCodec(TypeCodecs.TEXT, Lists.newArrayList("NULL", "NADA")); + assertThat(codec) + .convertsFromExternal("foo") + .toInternal("foo") + .convertsFromExternal("") + .toInternal("") + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("NADA") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + StringToStringCodec codec = new StringToStringCodec(TypeCodecs.TEXT, Collections.emptyList()); + assertThat(codec) + .convertsFromInternal("foo") + .toExternal("foo") + .convertsFromInternal("") + .toExternal("") + .convertsFromInternal(null) + .toExternal(null); + } + + @Test + void should_convert_from_valid_internal_with_custom_null_string() { + StringToStringCodec codec = + new StringToStringCodec(TypeCodecs.TEXT, Lists.newArrayList("NULL")); + assertThat(codec) + .convertsFromInternal("foo") + .toExternal("foo") + .convertsFromInternal("") + .toExternal("") + .convertsFromInternal("NULL") + .toExternal("NULL") + .convertsFromInternal(null) + .toExternal("NULL"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTupleCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTupleCodecTest.java new file mode 100644 index 0000000..909d35a --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToTupleCodecTest.java @@ -0,0 +1,149 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import com.datastax.oss.dsbulk.tests.driver.DriverUtils; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToTupleCodecTest { + + private TupleType tupleType; + + private StringToTupleCodec codec1; + private StringToTupleCodec codec2; + private StringToTupleCodec codec3; + + @BeforeEach + void setUp() { + tupleType = + DriverUtils.mockTupleType( + DefaultProtocolVersion.V4, CodecRegistry.DEFAULT, DataTypes.TIMESTAMP, DataTypes.TEXT); + ConversionContext context1 = new TextConversionContext().setNullStrings("NULL", ""); + ConversionContext context2 = new TextConversionContext().setAllowExtraFields(true); + ConversionContext context3 = new TextConversionContext().setAllowMissingFields(true); + codec1 = + (StringToTupleCodec) + new ConvertingCodecFactory(context1) + .createConvertingCodec(tupleType, GenericType.STRING, true); + codec2 = + (StringToTupleCodec) + new ConvertingCodecFactory(context2) + .createConvertingCodec(tupleType, GenericType.STRING, true); + codec3 = + (StringToTupleCodec) + new ConvertingCodecFactory(context3) + .createConvertingCodec(tupleType, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(codec1) + .convertsFromExternal("[\"2016-07-24T20:34:12.999\",\"+01:00\"]") + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal("['2016-07-24T20:34:12.999','+01:00']") + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal("[ \"2016-07-24T20:34:12.999\" , \"+01:00\" ]") + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal("[\"2016-07-24T20:34:12.999Z\",\"+01:00\"]") + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal("[null,\"\"]") + .toInternal(tupleType.newValue(null, "")) + .convertsFromExternal("[null,\"NULL\"]") + .toInternal(tupleType.newValue(null, "NULL")) + .convertsFromExternal("[null,null]") + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal("[,]") + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + // should allow extra elements + assertThat(codec2) + .convertsFromExternal("[\"2016-07-24T20:34:12.999\",\"+01:00\", 42]") + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .convertsFromExternal("[,\"\",\"\"]") + .toInternal(tupleType.newValue(null, "")) + .convertsFromExternal("[null,null,null]") + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal("[,,]") + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + // should allow missing elements + assertThat(codec3) + .convertsFromExternal("[\"2016-07-24T20:34:12.999\"]") + .toInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), null)) + .convertsFromExternal("[null]") + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal("[]") + .toInternal(tupleType.newValue(null, null)) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec1) + .convertsFromInternal( + tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "+01:00")) + .toExternal("[\"2016-07-24T20:34:12.999Z\",\"+01:00\"]") + .convertsFromInternal(tupleType.newValue(Instant.parse("2016-07-24T20:34:12.999Z"), "")) + .toExternal("[\"2016-07-24T20:34:12.999Z\",\"\"]") + .convertsFromInternal(tupleType.newValue(null, "")) + .toExternal("[null,\"\"]") + .convertsFromInternal(tupleType.newValue(null, null)) + .toExternal("[null,null]") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec1).cannotConvertFromExternal("{\"not a valid tuple\":42}"); + // should not allow missing elements + assertThat(codec2) + .cannotConvertFromExternal("[\"2016-07-24T20:34:12.999Z\"]") + .cannotConvertFromExternal("[]"); + // should not allow extra elements + assertThat(codec3).cannotConvertFromExternal("[\"2016-07-24T20:34:12.999Z\",\"+01:00\",42]"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUDTCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUDTCodecTest.java new file mode 100644 index 0000000..b85964f --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUDTCodecTest.java @@ -0,0 +1,227 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.dsbulk.codecs.api.ConversionContext; +import com.datastax.oss.dsbulk.codecs.api.ConvertingCodecFactory; +import com.datastax.oss.dsbulk.codecs.text.TextConversionContext; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StringToUDTCodecTest { + + // user types + + private final UserDefinedType udt1 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("\"F1A\"", DataTypes.INT) + .withField("f1b", DataTypes.mapOf(DataTypes.TEXT, DataTypes.DOUBLE)) + .build(); + + private final UserDefinedType udt2 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("f2a", udt1) + .withField("f2b", DataTypes.listOf(DataTypes.DATE)) + .build(); + + private final UserDefinedType udt3 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("f1", DataTypes.INT) + .withField("f2", DataTypes.INT) + .build(); + + private final UserDefinedType udt4 = + new UserDefinedTypeBuilder("ks", "udt") + .withField("f1", DataTypes.INT) + .withField("f2", DataTypes.INT) + .build(); + + // user type values + + private final UdtValue udt1Value = + udt1.newValue() + .setInt("\"F1A\"", 42) + .setMap("f1b", newMap("foo", 1234.56d, "", 0.12d), String.class, Double.class); + private final UdtValue udt1ValueEmpty = udt1.newValue().setToNull("\"F1A\"").setToNull("f1b"); + + private final UdtValue udt2Value = + udt2.newValue() + .setUdtValue("f2a", udt1Value) + .set("f2b", newList(LocalDate.of(2017, 9, 22)), TypeCodecs.listOf(TypeCodecs.DATE)); + private final UdtValue udt2ValueEmpty = udt2.newValue().setToNull("f2a").setToNull("f2b"); + + private final UdtValue udt3Value = udt3.newValue().setInt("f1", 42).setInt("f2", 42); + + private final UdtValue udt4Value = udt4.newValue().setInt("f1", 42).setInt("f2", 42); + private final UdtValue udt4ValuePartial = udt4.newValue().setInt("f1", 42); + private final UdtValue udt4ValueEmpty = udt4.newValue(); + + // codecs + + private StringToUDTCodec udtCodec1; + private StringToUDTCodec udtCodec2; + private StringToUDTCodec udtCodec3; + private StringToUDTCodec udtCodec4; + + @BeforeEach + void setUp() { + ConversionContext context1 = new TextConversionContext().setNullStrings("NULL", ""); + ConversionContext context2 = new TextConversionContext().setAllowExtraFields(true); + ConversionContext context3 = new TextConversionContext().setAllowMissingFields(true); + ConvertingCodecFactory codecFactory1 = new ConvertingCodecFactory(context1); + ConvertingCodecFactory codecFactory2 = new ConvertingCodecFactory(context2); + ConvertingCodecFactory codecFactory3 = new ConvertingCodecFactory(context3); + udtCodec1 = + (StringToUDTCodec) + codecFactory1.createConvertingCodec(udt1, GenericType.STRING, true); + udtCodec2 = + (StringToUDTCodec) + codecFactory1.createConvertingCodec(udt2, GenericType.STRING, true); + udtCodec3 = + (StringToUDTCodec) + codecFactory2.createConvertingCodec(udt3, GenericType.STRING, true); + udtCodec4 = + (StringToUDTCodec) + codecFactory3.createConvertingCodec(udt4, GenericType.STRING, true); + } + + @Test + void should_convert_from_valid_external() { + assertThat(udtCodec1) + .convertsFromExternal("{\"F1A\":42,\"f1b\":{\"foo\":1234.56,\"\":0.12}}") + .toInternal(udt1Value) + .convertsFromExternal("{'F1A':42,'f1b':{'foo':1234.56,'':0.12}}") + .toInternal(udt1Value) + .convertsFromExternal( + "{ \"f1b\" : { \"foo\" : \"1,234.56\" , \"\" : \"0000.12000\" } , \"F1A\" : \"42.00\" }") + .toInternal(udt1Value) + .convertsFromExternal("{ \"f1b\" : { } , \"F1A\" : null }") + .toInternal(udt1ValueEmpty) + .convertsFromExternal("{ \"f1b\" : null , \"F1A\" : null }") + .toInternal(udt1ValueEmpty) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + assertThat(udtCodec2) + .convertsFromExternal( + "{\"f2a\":{\"F1A\":42,\"f1b\":{\"foo\":1234.56,\"\":0.12}},\"f2b\":[\"2017-09-22\"]}") + .toInternal(udt2Value) + .convertsFromExternal( + "{'f2a':{'F1A':42,'f1b':{'foo':1234.56,'':0.12}},'f2b':['2017-09-22']}") + .toInternal(udt2Value) + .convertsFromExternal( + "{ \"f2b\" : [ \"2017-09-22\" ] , \"f2a\" : { \"f1b\" : { \"foo\" : \"1,234.56\" , \"\" : \"0000.12000\" } , \"F1A\" : \"42.00\" } }") + .toInternal(udt2Value) + .convertsFromExternal("{ \"f2b\" : null , \"f2a\" : null }") + .toInternal(udt2ValueEmpty) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + // should allow extra fields + assertThat(udtCodec3) + .convertsFromExternal("{\"f1\":42,\"f2\":42}") + .toInternal(udt3Value) + .convertsFromExternal("{\"f1\":42,\"f2\":42,\"f3\":42}") + .toInternal(udt3Value) + .convertsFromExternal("[42,42]") + .toInternal(udt3Value) + .convertsFromExternal("[42,42,42]") + .toInternal(udt3Value) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + // should allow missing fields + assertThat(udtCodec4) + .convertsFromExternal("{\"f1\":42,\"f2\":42}") + .toInternal(udt4Value) + .convertsFromExternal("{\"f1\":42}") + .toInternal(udt4ValuePartial) + .convertsFromExternal("{}") + .toInternal(udt4ValueEmpty) + .convertsFromExternal("[42,42]") + .toInternal(udt4Value) + .convertsFromExternal("[42]") + .toInternal(udt4ValuePartial) + .convertsFromExternal("[]") + .toInternal(udt4ValueEmpty) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(udtCodec1) + .convertsFromInternal(udt1Value) + .toExternal("{\"F1A\":42,\"f1b\":{\"foo\":1234.56,\"\":0.12}}") + .convertsFromInternal(udt1.newValue()) + .toExternal("{\"F1A\":null,\"f1b\":{}}") + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(udtCodec1) + .cannotConvertFromExternal("{\"F1A\":42}") + .cannotConvertFromExternal("[42]") + .cannotConvertFromExternal("{\"not a valid input\":\"foo\"}"); + // should not allow missing fields + assertThat(udtCodec3) + .cannotConvertFromExternal("{\"f1\":42}") + .cannotConvertFromExternal("[42]") + .cannotConvertFromExternal("[]"); + // should not allow extra fields + assertThat(udtCodec4) + .cannotConvertFromExternal("{\"f1\":42,\"f2\":42,\"f3\":42}") + .cannotConvertFromExternal("[42,42,42]"); + } + + @SuppressWarnings("SameParameterValue") + private static Map newMap(String k1, Double v1, String k2, Double v2) { + Map map = new LinkedHashMap<>(); + map.put(k1, v1); + map.put(k2, v2); + return map; + } + + private static List newList(LocalDate... elements) { + return Arrays.asList(elements); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUUIDCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUUIDCodecTest.java new file mode 100644 index 0000000..e8f807c --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUUIDCodecTest.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; +import static java.math.RoundingMode.HALF_EVEN; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.util.CodecUtils; +import com.datastax.oss.dsbulk.codecs.api.util.TimeUUIDGenerator; +import io.netty.util.concurrent.FastThreadLocal; +import java.text.NumberFormat; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class StringToUUIDCodecTest { + + private final FastThreadLocal numberFormat = + CodecUtils.getNumberFormatThreadLocal("#,###.##", US, HALF_EVEN, true); + + private final List nullStrings = Lists.newArrayList("NULL"); + + private StringToInstantCodec instantCodec = + new StringToInstantCodec( + CodecUtils.getTemporalFormat( + "yyyy-MM-dd'T'HH:mm:ss[.SSSSSSSSS]XXX", + UTC, + US, + MILLISECONDS, + EPOCH.atZone(UTC), + numberFormat, + true), + UTC, + EPOCH.atZone(UTC), + nullStrings); + + private final StringToUUIDCodec codec = + new StringToUUIDCodec(TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.MIN, nullStrings); + + @Test + void should_convert_from_valid_external() { + assertThat(codec) + .convertsFromExternal("a15341ec-ebef-4eab-b91d-ff16bf801a79") + .toInternal(UUID.fromString("a15341ec-ebef-4eab-b91d-ff16bf801a79")) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + + assertThat( + new StringToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.MIN, nullStrings)) + .convertsFromExternal("2017-12-05T12:44:36+01:00") + .toInternal( + Uuids.startOf( + ZonedDateTime.parse("2017-12-05T12:44:36+01:00").toInstant().toEpochMilli())); + assertThat( + new StringToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.MAX, nullStrings)) + .convertsFromExternal("2017-12-05T12:44:36.999999999+01:00") + .toInternal( + Uuids.endOf( + ZonedDateTime.parse("2017-12-05T12:44:36.999+01:00").toInstant().toEpochMilli())); + assertThat( + new StringToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.FIXED, nullStrings) + .externalToInternal("2017-12-05T12:44:36+01:00") + .timestamp()) + .isEqualTo( + Uuids.startOf( + ZonedDateTime.parse("2017-12-05T12:44:36+01:00").toInstant().toEpochMilli()) + .timestamp()); + assertThat( + new StringToUUIDCodec( + TypeCodecs.UUID, instantCodec, TimeUUIDGenerator.RANDOM, nullStrings) + .externalToInternal("2017-12-05T12:44:36+01:00") + .timestamp()) + .isEqualTo( + Uuids.startOf( + ZonedDateTime.parse("2017-12-05T12:44:36+01:00").toInstant().toEpochMilli()) + .timestamp()); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(codec) + .convertsFromInternal(UUID.fromString("a15341ec-ebef-4eab-b91d-ff16bf801a79")) + .toExternal("a15341ec-ebef-4eab-b91d-ff16bf801a79"); + } + + @Test + void should_not_convert_from_invalid_external() { + assertThat(codec).cannotConvertFromExternal("not a valid UUID"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUnknownTypeCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUnknownTypeCodecTest.java new file mode 100644 index 0000000..d4c118f --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToUnknownTypeCodecTest.java @@ -0,0 +1,135 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.driver.shaded.guava.common.collect.Lists.newArrayList; +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import org.junit.jupiter.api.Test; + +public class StringToUnknownTypeCodecTest { + + private FruitCodec targetCodec = new FruitCodec(); + private List nullStrings = newArrayList("NULL"); + private Fruit banana = new Fruit("banana"); + + @Test + void should_convert_from_valid_external() { + StringToUnknownTypeCodec codec = + new StringToUnknownTypeCodec<>(targetCodec, nullStrings); + assertThat(codec) + .convertsFromExternal("banana") + .toInternal(banana) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + StringToUnknownTypeCodec codec = + new StringToUnknownTypeCodec<>(targetCodec, nullStrings); + assertThat(codec).convertsFromInternal(banana).toExternal("banana"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToUnknownTypeCodec codec = + new StringToUnknownTypeCodec<>(targetCodec, nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid fruit literal"); + } + + private static final DataType FRUIT_TYPE = DataTypes.custom("com.datastax.dse.FruitType"); + + public static class Fruit { + + final String name; + + public Fruit(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Fruit)) { + return false; + } + Fruit fruit = (Fruit) o; + return Objects.equals(name, fruit.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } + + public static class FruitCodec implements TypeCodec { + + private static final GenericType JAVA_TYPE = GenericType.of(Fruit.class); + + @Override + public Fruit parse(String value) { + if (value != null && value.equalsIgnoreCase("banana")) { + return new Fruit(value); + } + throw new IllegalArgumentException("Unknown fruit: " + value); + } + + @NonNull + @Override + public String format(Fruit value) { + return value.name; + } + + @NonNull + @Override + public GenericType getJavaType() { + return JAVA_TYPE; + } + + @NonNull + @Override + public DataType getCqlType() { + return FRUIT_TYPE; + } + + @Override + public ByteBuffer encode(Fruit value, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("irrelevant"); + } + + @Override + public Fruit decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("irrelevant"); + } + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToVectorCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToVectorCodecTest.java new file mode 100644 index 0000000..097e836 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/StringToVectorCodecTest.java @@ -0,0 +1,76 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.oss.driver.api.core.data.CqlVector; +import com.datastax.oss.driver.api.core.type.CqlVectorType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.type.codec.CqlVectorCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.ArrayList; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class StringToVectorCodecTest { + + private final ArrayList values = Lists.newArrayList(1.1f, 2.2f, 3.3f, 4.4f, 5.5f); + private final CqlVector vector = CqlVector.builder().addAll(values).build(); + private final CqlVectorCodec vectorCodec = + new CqlVectorCodec(new CqlVectorType(DataTypes.FLOAT, 5), TypeCodecs.FLOAT); + + private final StringToVectorCodec dsbulkCodec = + new StringToVectorCodec(vectorCodec, Lists.newArrayList("NULL")); + + @Test + void should_convert_from_valid_external() { + assertThat(dsbulkCodec) + .convertsFromExternal(vectorCodec.format(vector)) // standard pattern + .toInternal(vector) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + assertThat(dsbulkCodec) + .convertsFromInternal(vector) + .toExternal(vectorCodec.format(vector)) + .convertsFromInternal(null) + .toExternal("NULL"); + } + + @Test + @Disabled("Requires driver support for validating a CqlVector against a CqlVectorType") + void should_not_convert_from_invalid_internal() { + // Too few values to match dimensions + ArrayList tooMany = Lists.newArrayList(values); + tooMany.add(6.6f); + ArrayList tooFew = Lists.newArrayList(values); + tooFew.remove(0); + + assertThat(dsbulkCodec) + .cannotConvertFromInternal(CqlVector.builder().addAll(tooMany).build()) + .cannotConvertFromInternal(CqlVector.builder().addAll(tooFew).build()) + .cannotConvertFromInternal("not a valid vector"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToDateRangeCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToDateRangeCodecTest.java new file mode 100644 index 0000000..c9fa033 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToDateRangeCodecTest.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.time.DateRange; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.text.ParseException; +import java.util.List; +import org.junit.jupiter.api.Test; + +class StringToDateRangeCodecTest { + + private List nullStrings = Lists.newArrayList("NULL"); + private DateRange dateRange; + + StringToDateRangeCodecTest() { + try { + dateRange = DateRange.parse("[* TO 2014-12-01]"); + } catch (ParseException e) { + // swallow; can't happen. + } + } + + @Test + void should_convert_from_valid_external() { + + StringToDateRangeCodec codec = new StringToDateRangeCodec(nullStrings); + assertThat(codec) + .convertsFromExternal("[* TO 2014-12-01]") + .toInternal(dateRange) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + StringToDateRangeCodec codec = new StringToDateRangeCodec(nullStrings); + assertThat(codec).convertsFromInternal(dateRange).toExternal("[* TO 2014-12-01]"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToDateRangeCodec codec = new StringToDateRangeCodec(nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid date range literal"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToLineStringCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToLineStringCodecTest.java new file mode 100644 index 0000000..dda16b3 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToLineStringCodecTest.java @@ -0,0 +1,92 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.geometry.LineString; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultLineString; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPoint; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownBinaryGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownTextGeoFormat; +import java.util.List; +import org.junit.jupiter.api.Test; + +class StringToLineStringCodecTest { + + private final List nullStrings = Lists.newArrayList("NULL"); + private final LineString lineString = + new DefaultLineString( + new DefaultPoint(30, 10), new DefaultPoint(10, 30), new DefaultPoint(40, 40)); + + @Test + void should_convert_from_valid_external() { + StringToLineStringCodec codec = + new StringToLineStringCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromExternal("'LINESTRING (30 10, 10 30, 40 40)'") + .toInternal(lineString) + .convertsFromExternal(" linestring (30 10, 10 30, 40 40) ") + .toInternal(lineString) + .convertsFromExternal( + "{\"type\":\"LineString\",\"coordinates\":[[30.0,10.0],[10.0,30.0],[40.0,40.0]]}") + .toInternal(lineString) + .convertsFromExternal( + "AQIAAAADAAAAAAAAAAAAPkAAAAAAAAAkQAAAAAAAACRAAAAAAAAAPkAAAAAAAABEQAAAAAAAAERA") + .toInternal(lineString) + .convertsFromExternal( + "0x0102000000030000000000000000003e40000000000000244000000000000024400000000000003e4000000000000044400000000000004440") + .toInternal(lineString) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + StringToLineStringCodec codec = + new StringToLineStringCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(lineString) + .toExternal("LINESTRING (30 10, 10 30, 40 40)"); + codec = new StringToLineStringCodec(JsonGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(lineString) + .toExternal( + "{\"type\":\"LineString\",\"coordinates\":[[30.0,10.0],[10.0,30.0],[40.0,40.0]]}"); + codec = new StringToLineStringCodec(WellKnownBinaryGeoFormat.BASE64_INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(lineString) + .toExternal("AQIAAAADAAAAAAAAAAAAPkAAAAAAAAAkQAAAAAAAACRAAAAAAAAAPkAAAAAAAABEQAAAAAAAAERA"); + codec = new StringToLineStringCodec(WellKnownBinaryGeoFormat.HEX_INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(lineString) + .toExternal( + "0x0102000000030000000000000000003e40000000000000244000000000000024400000000000003e4000000000000044400000000000004440"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToLineStringCodec codec = + new StringToLineStringCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid linestring literal"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPointCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPointCodecTest.java new file mode 100644 index 0000000..fd2a2a0 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPointCodecTest.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.geometry.Point; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPoint; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownBinaryGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownTextGeoFormat; +import java.util.List; +import org.junit.jupiter.api.Test; + +class StringToPointCodecTest { + + private final List nullStrings = Lists.newArrayList("NULL"); + private final Point point = new DefaultPoint(-1.1, -2.2); + + @Test + void should_convert_from_valid_external() { + StringToPointCodec codec = new StringToPointCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromExternal("'POINT (-1.1 -2.2)'") + .toInternal(point) + .convertsFromExternal(" point (-1.1 -2.2) ") + .toInternal(point) + .convertsFromExternal("{\"type\":\"Point\",\"coordinates\":[-1.1,-2.2]}") + .toInternal(point) + .convertsFromExternal("AQEAAACamZmZmZnxv5qZmZmZmQHA") + .toInternal(point) + .convertsFromExternal("0x01010000009a9999999999f1bf9a999999999901c0") + .toInternal(point) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + StringToPointCodec codec = new StringToPointCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(point).toExternal("POINT (-1.1 -2.2)"); + codec = new StringToPointCodec(JsonGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(point) + .toExternal("{\"type\":\"Point\",\"coordinates\":[-1.1,-2.2]}"); + codec = new StringToPointCodec(WellKnownBinaryGeoFormat.BASE64_INSTANCE, nullStrings); + assertThat(codec).convertsFromInternal(point).toExternal("AQEAAACamZmZmZnxv5qZmZmZmQHA"); + codec = new StringToPointCodec(WellKnownBinaryGeoFormat.HEX_INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(point) + .toExternal("0x01010000009a9999999999f1bf9a999999999901c0"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToPointCodec codec = new StringToPointCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid point literal"); + } +} diff --git a/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPolygonCodecTest.java b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPolygonCodecTest.java new file mode 100644 index 0000000..3f87951 --- /dev/null +++ b/text/src/test/java/com/datastax/oss/dsbulk/codecs/text/string/dse/StringToPolygonCodecTest.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * 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.datastax.oss.dsbulk.codecs.text.string.dse; + +import static com.datastax.oss.dsbulk.tests.assertions.TestAssertions.assertThat; + +import com.datastax.dse.driver.api.core.data.geometry.Polygon; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPoint; +import com.datastax.dse.driver.internal.core.data.geometry.DefaultPolygon; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.dsbulk.codecs.api.format.geo.JsonGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownBinaryGeoFormat; +import com.datastax.oss.dsbulk.codecs.api.format.geo.WellKnownTextGeoFormat; +import java.util.List; +import org.junit.jupiter.api.Test; + +class StringToPolygonCodecTest { + + private final List nullStrings = Lists.newArrayList("NULL"); + private final Polygon polygon = + new DefaultPolygon( + new DefaultPoint(30, 10), + new DefaultPoint(10, 20), + new DefaultPoint(20, 40), + new DefaultPoint(40, 40)); + + @Test + void should_convert_from_valid_external() { + StringToPolygonCodec codec = + new StringToPolygonCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromExternal("'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))'") + .toInternal(polygon) + .convertsFromExternal(" polygon ((30 10, 40 40, 20 40, 10 20, 30 10)) ") + .toInternal(polygon) + .convertsFromExternal( + "{\"type\":\"Polygon\",\"coordinates\":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]]}") + .toInternal(polygon) + .convertsFromExternal( + "AQMAAAABAAAABQAAAAAAAAAAAD5AAAAAAAAAJEAAAAAAAABEQAAAAAAAAERAAAAAAAAANEAAAAAAAABEQAAAAAAAACRAAAAAAAAANEAAAAAAAAA+QAAAAAAAACRA") + .toInternal(polygon) + .convertsFromExternal( + "0x010300000001000000050000000000000000003e4000000000000024400000000000004440000000000000444" + + "000000000000034400000000000004440000000000000244000000000000034400000000000003e400000000000002440") + .toInternal(polygon) + .convertsFromExternal(null) + .toInternal(null) + .convertsFromExternal("") + .toInternal(null) + .convertsFromExternal("NULL") + .toInternal(null); + } + + @Test + void should_convert_from_valid_internal() { + StringToPolygonCodec codec = + new StringToPolygonCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(polygon) + .toExternal("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"); + codec = new StringToPolygonCodec(JsonGeoFormat.INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(polygon) + .toExternal( + "{\"type\":\"Polygon\",\"coordinates\":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]]}"); + codec = new StringToPolygonCodec(WellKnownBinaryGeoFormat.BASE64_INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(polygon) + .toExternal( + "AQMAAAABAAAABQAAAAAAAAAAAD5AAAAAAAAAJEAAAAAAAABEQAAAAAAAAERAAAAAAAAANEAAAAAAAABEQAAAAAAAACRAAAAAAAAANEAAAAAAAAA+QAAAAAAAACRA"); + codec = new StringToPolygonCodec(WellKnownBinaryGeoFormat.HEX_INSTANCE, nullStrings); + assertThat(codec) + .convertsFromInternal(polygon) + .toExternal( + "0x010300000001000000050000000000000000003e4000000000000024400000000000004440000000000000444" + + "000000000000034400000000000004440000000000000244000000000000034400000000000003e400000000000002440"); + } + + @Test + void should_not_convert_from_invalid_external() { + StringToPolygonCodec codec = + new StringToPolygonCodec(WellKnownTextGeoFormat.INSTANCE, nullStrings); + assertThat(codec).cannotConvertFromExternal("not a valid polygon literal"); + } +} diff --git a/text/src/test/resources/logback-test.xml b/text/src/test/resources/logback-test.xml new file mode 100644 index 0000000..eb7bd59 --- /dev/null +++ b/text/src/test/resources/logback-test.xml @@ -0,0 +1,28 @@ + + + + + + %-5level [%thread] %logger{40} - %msg%n + + + + + +