diff --git a/connector/pom.xml b/connector/pom.xml index 2cde763..5ab4bfa 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -28,6 +28,10 @@ io.openmessaging openmessaging-api + + junit + junit + \ No newline at end of file diff --git a/connector/src/main/java/io/openmessaging/connector/api/PositionStorageReader.java b/connector/src/main/java/io/openmessaging/connector/api/PositionStorageReader.java index 80df7f4..24157f7 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/PositionStorageReader.java +++ b/connector/src/main/java/io/openmessaging/connector/api/PositionStorageReader.java @@ -30,7 +30,7 @@ public interface PositionStorageReader { /** - * Get the position for the specified partition. + * Get the position for the specified queueId. * * @param partition * @return diff --git a/connector/src/main/java/io/openmessaging/connector/api/common/QueueMetaData.java b/connector/src/main/java/io/openmessaging/connector/api/common/QueueMetaData.java index d9cc238..d58e255 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/common/QueueMetaData.java +++ b/connector/src/main/java/io/openmessaging/connector/api/common/QueueMetaData.java @@ -46,24 +46,25 @@ public void setShardingKey(String shardingKey) { this.shardingKey = shardingKey; } - @Override public String toString() { + @Override + public String toString() { return "QueueMetaData{" + "queueName='" + queueName + '\'' + ", shardingKey='" + shardingKey + '\'' + '}'; } - @Override public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof QueueMetaData)) - return false; - QueueMetaData data = (QueueMetaData) o; + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (!(o instanceof QueueMetaData)) { return false; } + QueueMetaData data = (QueueMetaData)o; return Objects.equals(queueName, data.queueName) && Objects.equals(shardingKey, data.shardingKey); } - @Override public int hashCode() { + @Override + public int hashCode() { return Objects.hash(queueName, shardingKey); } } diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/DataEntry.java b/connector/src/main/java/io/openmessaging/connector/api/data/DataEntry.java index 53f7675..4ac9c55 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/data/DataEntry.java +++ b/connector/src/main/java/io/openmessaging/connector/api/data/DataEntry.java @@ -17,9 +17,12 @@ package io.openmessaging.connector.api.data; -import java.util.Arrays; import java.util.Objects; +import io.openmessaging.connector.api.header.DataHeaders; +import io.openmessaging.connector.api.header.Header; +import io.openmessaging.connector.api.header.Headers; + /** * Base class for records containing data to be copied to/from message queue. * @@ -27,58 +30,63 @@ * @since OMS 0.1.0 */ public abstract class DataEntry { - - public DataEntry(Long timestamp, - EntryType entryType, - String queueName, - Schema schema, - Object[] payload) { - this(timestamp, entryType, queueName, schema, null, payload); - } - - public DataEntry(Long timestamp, - EntryType entryType, - String queueName, - Schema schema, - String shardingKey, - Object[] payload) { - this.timestamp = timestamp; - this.entryType = entryType; - this.queueName = queueName; - this.schema = schema; - this.shardingKey = shardingKey; - this.payload = payload; - } - /** * Timestamp of the data entry. */ private Long timestamp; - - /** - * Type of the data entry. - */ - private EntryType entryType; - /** * Related queueName. */ private String queueName; - /** * Used for shard to related queue/partition. */ private String shardingKey; - /** - * Schema of the data entry. + * {@link EntryType} of the {@link DataEntry} */ - private Schema schema; - + private EntryType entryType; + /** + * Definition data key. + */ + private MetaAndData key; /** - * Payload of the data entry. + * Definition data value. */ - private Object[] payload; + private MetaAndData value; + /** + * The Headers of data. + */ + private Headers headers; + + public DataEntry(Long timestamp, + String queueName, + String shardingKey, + EntryType entryType, + MetaAndData key, + MetaAndData value) { + this(timestamp, queueName, shardingKey, entryType, key, value, new DataHeaders()); + } + + public DataEntry(Long timestamp, + String queueName, + String shardingKey, + EntryType entryType, + MetaAndData key, + MetaAndData value, + Iterable
headers) { + this.timestamp = timestamp; + this.queueName = queueName; + this.shardingKey = shardingKey; + this.entryType = entryType; + this.key = key; + this.value = value; + if (headers instanceof DataHeaders) { + this.headers = (DataHeaders)headers; + } else { + this.headers = new DataHeaders(headers); + } + } public Long getTimestamp() { return timestamp; @@ -104,20 +112,28 @@ public void setQueueName(String queueName) { this.queueName = queueName; } - public Schema getSchema() { - return schema; + public MetaAndData getKey() { + return key; + } + + public void setKey(MetaAndData meta) { + this.key = meta; + } + + public MetaAndData getValue() { + return value; } - public void setSchema(Schema schema) { - this.schema = schema; + public void setValue(MetaAndData value) { + this.value = value; } - public Object[] getPayload() { - return payload; + public Headers getHeaders() { + return headers; } - public void setPayload(Object[] payload) { - this.payload = payload; + public void setHeaders(Headers headers) { + this.headers = headers; } public String getShardingKey() { @@ -128,34 +144,48 @@ public void setShardingKey(String shardingKey) { this.shardingKey = shardingKey; } - @Override public String toString() { + @Override + public String toString() { return "DataEntry{" + - "timestamp=" + timestamp + - ", entryType=" + entryType + - ", queueName='" + queueName + '\'' + - ", shardingKey='" + shardingKey + '\'' + - ", schema=" + schema + - ", payload=" + Arrays.toString(payload) + + "queueName='" + this.queueName + '\'' + + ", shardingKey='" + this.shardingKey + + ", entryType='" + this.entryType + '\'' + + ", key='" + this.key + '\'' + + ", value='" + this.value + '\'' + + ", timestamp='" + timestamp + '\'' + + ", headers'=" + headers + '\'' + '}'; } - @Override public boolean equals(Object o) { - if (this == o) + @Override + public boolean equals(Object o) { + if (this == o) { return true; - if (!(o instanceof DataEntry)) + } + if (o == null || getClass() != o.getClass()) { return false; - DataEntry entry = (DataEntry) o; - return Objects.equals(timestamp, entry.timestamp) && - entryType == entry.entryType && - Objects.equals(queueName, entry.queueName) && - Objects.equals(shardingKey, entry.shardingKey) && - Objects.equals(schema, entry.schema) && - Arrays.equals(payload, entry.payload); - } - - @Override public int hashCode() { - int result = Objects.hash(timestamp, entryType, queueName, shardingKey, schema); - result = 31 * result + Arrays.hashCode(payload); + } + + DataEntry that = (DataEntry)o; + + return Objects.equals(this.shardingKey, that.shardingKey) + && Objects.equals(this.queueName, that.queueName) + && Objects.equals(this.entryType, that.entryType) + && Objects.equals(this.key, that.key) + && Objects.equals(this.value, that.value) + && Objects.equals(this.timestamp, that.timestamp) + && Objects.equals(this.headers, that.headers); + } + + @Override + public int hashCode() { + int result = this.queueName != null ? this.queueName.hashCode() : 0; + result = 31 * result + (this.shardingKey != null ? this.shardingKey.hashCode() : 0); + result = 31 * result + (this.entryType != null ? entryType.hashCode() : 0); + result = 31 * result + (this.key != null ? this.key.hashCode() : 0); + result = 31 * result + (this.value != null ? this.value.hashCode() : 0); + result = 31 * result + (this.timestamp != null ? this.timestamp.hashCode() : 0); + result = 31 * result + this.headers.hashCode(); return result; } } diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/DataEntryBuilder.java b/connector/src/main/java/io/openmessaging/connector/api/data/DataEntryBuilder.java index 9a80473..2d897fd 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/data/DataEntryBuilder.java +++ b/connector/src/main/java/io/openmessaging/connector/api/data/DataEntryBuilder.java @@ -17,7 +17,14 @@ package io.openmessaging.connector.api.data; +import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +import io.openmessaging.connector.api.header.DataHeaders; +import io.openmessaging.connector.api.header.Header; +import io.openmessaging.connector.api.header.Headers; /** * Use DataEntryBuilder to build SourceDataEntry or SinkDataEntry. @@ -32,34 +39,68 @@ public class DataEntryBuilder { */ private Long timestamp; - /** - * Type of the data entry. - */ - private EntryType entryType; - /** * Related queue name. */ private String queueName; /** - * Used for set target partition/queue of a related queue. + * Used for shard to related queue/partition. */ private String shardingKey; /** - * Schema of the data entry. + * Type of the data entry. */ - private Schema schema; + private EntryType entryType; /** - * Payload of the data entry. + * Key of the data entry. */ - private Object[] payload; + private MetaAndData key; - public DataEntryBuilder(Schema schema) { - this.schema = schema; - this.payload = new Object[schema.getFields().size()]; + /** + * Value of the data entry. + */ + private MetaAndData value; + + /** + * The Headers of data. + */ + private Headers headers; + + public DataEntryBuilder() { + this.headers = new DataHeaders(); + } + + public DataEntryBuilder(Meta valueMeta) { + this(null, new MetaAndData(valueMeta)); + } + + public DataEntryBuilder(MetaAndData valueMetaAndData) { + this(null, valueMetaAndData); + } + + public DataEntryBuilder(Meta keyMeta, Meta valueMeta) { + this(new MetaAndData(keyMeta), new MetaAndData(valueMeta)); + } + + public DataEntryBuilder(MetaAndData keyMetaAndData, MetaAndData valueMetaAndData) { + this.key = keyMetaAndData; + this.value = valueMetaAndData; + this.headers = new DataHeaders(); + } + + public static DataEntryBuilder newDataEntryBuilder(Meta keyMeta, Meta valueMeta) { + return new DataEntryBuilder(keyMeta, valueMeta); + } + + public static DataEntryBuilder newDataEntryBuilder(Meta valueMeta) { + return new DataEntryBuilder(valueMeta); + } + + public static DataEntryBuilder newDataEntryBuilder() { + return new DataEntryBuilder(); } public DataEntryBuilder timestamp(Long timestamp) { @@ -67,6 +108,11 @@ public DataEntryBuilder timestamp(Long timestamp) { return this; } + public DataEntryBuilder shardingKey(String shardingKey) { + this.shardingKey = shardingKey; + return this; + } + public DataEntryBuilder entryType(EntryType entryType) { this.entryType = entryType; return this; @@ -77,32 +123,247 @@ public DataEntryBuilder queue(String queueName) { return this; } - public DataEntryBuilder shardingKey(String shardingKey) { - this.shardingKey = shardingKey; + // headers + + public DataEntryBuilder header(Header header) { + this.headers.add(header); + return this; + } + + public DataEntryBuilder header(String key, Meta meta, Object value) { + this.headers.add(key, meta, value); + return this; + } + + public DataEntryBuilder header(String key, String value) { + this.headers.addString(key, value); + return this; + } + + public DataEntryBuilder header(String key, boolean value) { + this.headers.addBoolean(key, value); + return this; + } + + public DataEntryBuilder header(String key, byte value) { + this.headers.addByte(key, value); + return this; + } + + public DataEntryBuilder header(String key, short value) { + this.headers.addShort(key, value); + return this; + } + + public DataEntryBuilder header(String key, int value) { + this.headers.addInt(key, value); + return this; + } + + public DataEntryBuilder header(String key, long value) { + this.headers.addLong(key, value); + return this; + } + + public DataEntryBuilder header(String key, float value) { + this.headers.addFloat(key, value); + return this; + } + + public DataEntryBuilder header(String key, double value) { + this.headers.addDouble(key, value); + return this; + } + + public DataEntryBuilder header(String key, byte[] value) { + this.headers.addBytes(key, value); return this; } - public DataEntryBuilder putFiled(String fieldName, Object value) { + public DataEntryBuilder header(String key, List value, Meta meta) { + this.headers.addList(key, value, meta); + return this; + } + + public DataEntryBuilder header(String key, Map value, Meta meta) { + this.headers.addMap(key, value, meta); + return this; + } + + public DataEntryBuilder header(String key, Struct value) { + this.headers.addStruct(key, value); + return this; + } - Field field = lookupField(fieldName); - payload[field.getIndex()] = value; + public DataEntryBuilder header(String key, BigDecimal value) { + this.headers.addDecimal(key, value); + return this; + } + + public DataEntryBuilder header(String key, java.util.Date value) { + this.headers.addDate(key, value); + return this; + } + + public DataEntryBuilder keyMeta(Meta keyMeta) { + MetaAndData tmpKey = new MetaAndData(keyMeta); + if (this.key != null && this.key.getData() != null) { + tmpKey.putData(this.key.getData()); + } + this.key = tmpKey; + return this; + } + + public DataEntryBuilder valueMeta(Meta valueMeta) { + MetaAndData tmpValue = new MetaAndData(valueMeta); + if (this.value != null && this.value.getData() != null) { + tmpValue.putData(this.value.getData()); + } + this.value = tmpValue; + return this; + } + + // base + + public DataEntryBuilder keyData(Object data) { + switch (this.key.getMeta().getType()) { + case STRUCT: + List fields = null; + if (data instanceof Struct) { + fields = ((Struct)data).getFields(); + } else if (data instanceof MetaAndData) { + fields = ((MetaAndData)data).getMeta().getFields(); + } else { + MetaAndData strData = MetaAndData.getMetaDataFromString(data.toString()); + fields = strData.getMeta().getFields(); + } + if (fields != null) { + for (int i = 0; i < fields.size(); i++) { + String fieldName = fields.get(i).name(); + keyData(fieldName, ((Struct)data).getObject(fieldName)); + } + } + break; + case MAP: + keyData((Map)data); + break; + case ARRAY: + keyData((List)data); + break; + default: + this.key.putData(data); + break; + } + return this; + } + + public DataEntryBuilder valueData(Object data) { + switch (this.value.getMeta().getType()) { + case STRUCT: + List fields = null; + if (data instanceof Struct) { + fields = ((Struct)data).getFields(); + } else if (data instanceof MetaAndData) { + fields = ((MetaAndData)data).getMeta().getFields(); + } else { + MetaAndData strData = MetaAndData.getMetaDataFromString(data.toString()); + fields = strData.getMeta().getFields(); + } + if (fields != null) { + for (int i = 0; i < fields.size(); i++) { + String fieldName = fields.get(i).name(); + valueData(fieldName, ((Struct)data).getObject(fieldName)); + } + } + break; + case MAP: + valueData((Map)data); + break; + case ARRAY: + valueData((List)data); + break; + default: + this.value.putData(data); + break; + } + return this; + } + + // map + + public DataEntryBuilder keyData(Object key, Object value) { + this.key.putData(key, value); + return this; + } + + public DataEntryBuilder keyData(Map map) { + this.key.putData(map); + return this; + } + + public DataEntryBuilder valueData(Object key, Object value) { + this.value.putData(key, value); + return this; + } + + public DataEntryBuilder valueData(Map map) { + this.value.putData(map); + return this; + } + + // array + + public DataEntryBuilder keyData(List elements) { + this.key.putData(elements); + return this; + } + + public DataEntryBuilder valueData(List elements) { + this.value.putData(elements); + return this; + } + + // struct + + public DataEntryBuilder keyData(String fieldName, Object value) { + this.key.putData(fieldName, value); + return this; + } + + public DataEntryBuilder keyData(Field field, Object value) { + this.key.putData(field, value); + return this; + } + + public DataEntryBuilder key(MetaAndData metaAndData) { + this.key = metaAndData; + return this; + } + + public DataEntryBuilder valueData(String fieldName, Object data) { + this.value.putData(fieldName, data); + return this; + } + + public DataEntryBuilder valueData(Field field, Object data) { + this.value.putData(field, data); + return this; + } + + public DataEntryBuilder value(MetaAndData metaAndData) { + this.value = metaAndData; return this; } public SourceDataEntry buildSourceDataEntry(ByteBuffer sourcePartition, ByteBuffer sourcePosition) { - return new SourceDataEntry(sourcePartition, sourcePosition, timestamp, entryType, queueName, schema, shardingKey, payload); + return new SourceDataEntry(sourcePartition, sourcePosition, timestamp, queueName, shardingKey, entryType, key, + value, headers); } public SinkDataEntry buildSinkDataEntry(Long queueOffset) { - return new SinkDataEntry(queueOffset, timestamp, entryType, queueName, schema, shardingKey, payload); + return new SinkDataEntry(queueOffset, timestamp, queueName, shardingKey, entryType, key, value, headers); } - private Field lookupField(String fieldName) { - Field field = schema.getField(fieldName); - if (field == null) - throw new RuntimeException(fieldName + " is not a valid field name"); - return field; - } } diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Date.java b/connector/src/main/java/io/openmessaging/connector/api/data/Date.java new file mode 100644 index 0000000..72a43b6 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Date.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.data; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + *

+ * A date representing a calendar day with no time of day or timezone. The corresponding Java type is a java.util.Date + * with hours, minutes, seconds, milliseconds set to 0. The underlying representation is an integer representing the + * number of standardized days (based on a number of milliseconds with 24 hours/day, 60 minutes/hour, 60 seconds/minute, + * 1000 milliseconds/second with n) since Unix epoch. + *

+ */ +public class Date { + public static final String LOGICAL_NAME = "io.openmessaging.connector.api.data.Date"; + + private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + /** + * Returns a MetaBuilder for a Date. By returning a MetaBuilder you can override additional meta settings such + * as required/optional, default value, and documentation. + * @return a MetaBuilder + */ + public static MetaBuilder builder() { + return MetaBuilder.int32() + .name(LOGICAL_NAME); + } + + public static final Meta META = builder().meta(); + + /** + * Convert a value from its logical format (Date) to it's encoded format. + * @param value the logical value + * @return the encoded value + */ + public static int fromLogical(Meta meta, java.util.Date value) { + if (!(LOGICAL_NAME.equals(meta.getName()))) { + throw new RuntimeException("Requested conversion of Date object but the meta does not match."); + } + Calendar calendar = Calendar.getInstance(UTC); + calendar.setTime(value); + if (calendar.get(Calendar.HOUR_OF_DAY) != 0 || calendar.get(Calendar.MINUTE) != 0 || + calendar.get(Calendar.SECOND) != 0 || calendar.get(Calendar.MILLISECOND) != 0) { + throw new RuntimeException("RocketMQ Connect Date type should not have any time fields set to non-zero values."); + } + long unixMillis = calendar.getTimeInMillis(); + return (int) (unixMillis / MILLIS_PER_DAY); + } + + public static java.util.Date toLogical(Meta meta, int value) { + if (!(LOGICAL_NAME.equals(meta.getName()))) { + throw new RuntimeException("Requested conversion of Date object but the meta does not match."); + } + return new java.util.Date(value * MILLIS_PER_DAY); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Decimal.java b/connector/src/main/java/io/openmessaging/connector/api/data/Decimal.java new file mode 100644 index 0000000..704794d --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Decimal.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.data; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + *

+ * An arbitrary-precision signed decimal number. The value is unscaled * 10 ^ -scale where: + *

    + *
  • unscaled is an integer
  • + *
  • scale is an integer representing how many digits the decimal point should be shifted on the unscaled value
  • + *
+ *

+ *

+ * Decimal does not provide a fixed meta because it is parameterized by the scale, which is fixed on the meta + * rather than being part of the value. + *

+ *

+ * The underlying representation of this type is bytes containing a two's complement integer + *

+ */ +public class Decimal { + public static final String LOGICAL_NAME = "io.openmessaging.connector.api.data.Decimal"; + public static final String SCALE_FIELD = "scale"; + + /** + * Returns a MetaBuilder for a Decimal with the given scale factor. By returning a MetaBuilder you can override + * additional meta settings such as required/optional, default value, and documentation. + * @param scale the scale factor to apply to unscaled values + * @return a MetaBuilder + */ + public static MetaBuilder builder(int scale) { + return MetaBuilder.bytes() + .name(LOGICAL_NAME) + .parameter(SCALE_FIELD, Integer.toString(scale)); + } + + public static Meta meta(int scale) { + return builder(scale).build(); + } + + /** + * Convert a value from its logical format (BigDecimal) to it's encoded format. + * @param value the logical value + * @return the encoded value + */ + public static byte[] fromLogical(Meta meta, BigDecimal value) { + if (value.scale() != scale(meta)) { + throw new RuntimeException("BigDecimal has mismatching scale value for given Decimal meta"); + } + return value.unscaledValue().toByteArray(); + } + + public static BigDecimal toLogical(Meta meta, byte[] value) { + return new BigDecimal(new BigInteger(value), scale(meta)); + } + + private static int scale(Meta meta) { + String scaleString = meta.getParameters().get(SCALE_FIELD); + if (scaleString == null) { + throw new RuntimeException("Invalid Decimal meta: scale parameter not found."); + } + try { + return Integer.parseInt(scaleString); + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid scale parameter found in Decimal meta: ", e); + } + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Field.java b/connector/src/main/java/io/openmessaging/connector/api/data/Field.java index 0b43046..3e6079e 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/data/Field.java +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Field.java @@ -20,7 +20,7 @@ import java.util.Objects; /** - * Filed of the schema. + * Filed of the meta. * * @version OMS 0.1.0 * @since OMS 0.1.0 @@ -28,71 +28,63 @@ public class Field { /** - * Index of a field. The index of fields in a schema should be unique and continuous。 + * Index of a field. The index of fields in a meta should be unique and continuous。 */ - private int index; + private final int index; /** - * The name of a file. Should be unique in a shcema. + * The name of a field. Should be unique in a meta. */ - private String name; + private final String name; /** - * The type of the file. + * The type of the field. */ - private FieldType type; - - public Field(int index, String name, FieldType type) { + private final Meta meta; + public Field(int index, String name, Meta meta) { this.index = index; this.name = name; - this.type = type; + this.meta = meta; } - public int getIndex() { + public int index() { return index; } - public void setIndex(int index) { - this.index = index; - } - - public String getName() { + public String name() { return name; } - public void setName(String name) { - this.name = name; - } - - public FieldType getType() { - return type; + public Meta meta() { + return meta; } - public void setType(FieldType type) { - this.type = type; - } - - @Override public String toString() { - return "Field{" + - "index=" + index + - ", name='" + name + '\'' + - ", type=" + type + - '}'; - } - - @Override public boolean equals(Object o) { - if (this == o) + @Override + public boolean equals(Object o) { + if (this == o) { return true; - if (!(o instanceof Field)) + } + if (o == null || getClass() != o.getClass()) { return false; + } Field field = (Field) o; - return index == field.index && + return Objects.equals(index, field.index) && Objects.equals(name, field.name) && - type == field.type; + Objects.equals(meta, field.meta); } - @Override public int hashCode() { - return Objects.hash(index, name, type); + @Override + public int hashCode() { + return Objects.hash(name, index, meta); + } + + @Override + public String toString() { + return "Field{" + + "name=" + name + + ", index=" + index + + ", meta=" + meta + + "}"; } } diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/FieldType.java b/connector/src/main/java/io/openmessaging/connector/api/data/FieldType.java deleted file mode 100644 index c53e07e..0000000 --- a/connector/src/main/java/io/openmessaging/connector/api/data/FieldType.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.openmessaging.connector.api.data; - -/** - * Define the field type. - * - * @version OMS 0.1.0 - * @since OMS 0.1.0 - */ -public enum FieldType { - - /** Integer */ - INT32, - - /** Long */ - INT64, - - /** BigInteger */ - BIG_INTEGER, - - /** Float */ - FLOAT32, - - /** Double */ - FLOAT64, - - /** Boolean */ - BOOLEAN, - - /** String */ - STRING, - - /** Byte */ - BYTES, - - /** List */ - ARRAY, - - /** Map */ - MAP, - - /** Date */ - DATETIME; -} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Meta.java b/connector/src/main/java/io/openmessaging/connector/api/data/Meta.java new file mode 100644 index 0000000..e78d9ba --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Meta.java @@ -0,0 +1,346 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.openmessaging.connector.api.data; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Meta + * + * @version OMS 0.1.0 + * @since OMS 0.1.0 + */ +public abstract class Meta { + /** + * Maps Types to a list of Java classes that can be used to represent them. + */ + private static final Map> META_TYPE_CLASSES = new EnumMap<>(Type.class); + /** + * Maps known logical types to a list of Java classes that can be used to represent them. + */ + private static final Map> LOGICAL_TYPE_CLASSES = new HashMap<>(); + /** + * Maps the Java classes to the corresponding Type. + */ + private static final Map, Type> JAVA_CLASS_META_TYPES = new HashMap<>(); + + static { + META_TYPE_CLASSES.put(Type.INT8, Collections.singletonList((Class)Byte.class)); + META_TYPE_CLASSES.put(Type.INT16, Collections.singletonList((Class)Short.class)); + META_TYPE_CLASSES.put(Type.INT32, Collections.singletonList((Class)Integer.class)); + META_TYPE_CLASSES.put(Type.INT64, Collections.singletonList((Class)Long.class)); + META_TYPE_CLASSES.put(Type.FLOAT32, Collections.singletonList((Class)Float.class)); + META_TYPE_CLASSES.put(Type.FLOAT64, Collections.singletonList((Class)Double.class)); + META_TYPE_CLASSES.put(Type.BOOLEAN, Collections.singletonList((Class)Boolean.class)); + META_TYPE_CLASSES.put(Type.STRING, Collections.singletonList((Class)String.class)); + // Bytes are special and have 2 representations. byte[] causes problems because it doesn't handle equals() and + // hashCode() like we want objects to, so we support both byte[] and ByteBuffer. Using plain byte[] can cause + // those methods to fail, so ByteBuffers are recommended + META_TYPE_CLASSES.put(Type.BYTES, Arrays.asList((Class)byte[].class, (Class)ByteBuffer.class)); + META_TYPE_CLASSES.put(Type.ARRAY, Collections.singletonList((Class)List.class)); + META_TYPE_CLASSES.put(Type.MAP, Collections.singletonList((Class)Map.class)); + META_TYPE_CLASSES.put(Type.STRUCT, Collections.singletonList((Class)Struct.class)); + + for (Map.Entry> metaClasses : META_TYPE_CLASSES.entrySet()) { + for (Class metaClass : metaClasses.getValue()) { + JAVA_CLASS_META_TYPES.put(metaClass, metaClasses.getKey()); + } + } + + LOGICAL_TYPE_CLASSES.put(Decimal.LOGICAL_NAME, Collections.singletonList((Class)BigDecimal.class)); + LOGICAL_TYPE_CLASSES.put(Date.LOGICAL_NAME, Collections.singletonList((Class)java.util.Date.class)); + LOGICAL_TYPE_CLASSES.put(Time.LOGICAL_NAME, Collections.singletonList((Class)java.util.Date.class)); + LOGICAL_TYPE_CLASSES.put(Timestamp.LOGICAL_NAME, Collections.singletonList((Class)java.util.Date.class)); + // We don't need to put these into JAVA_CLASS_META_TYPES since that's only used to determine metas for + // metaless data and logical types will have ambiguous metas (e.g. many of them use the same Java class) so + // they should not be used without metas. + } + + public static Meta INT8_META = MetaBuilder.int8().build(); + public static Meta INT16_META = MetaBuilder.int16().build(); + public static Meta INT32_META = MetaBuilder.int32().build(); + public static Meta INT64_META = MetaBuilder.int64().build(); + public static Meta FLOAT32_META = MetaBuilder.float32().build(); + public static Meta FLOAT64_META = MetaBuilder.float64().build(); + public static Meta BOOLEAN_META = MetaBuilder.bool().build(); + public static Meta STRING_META = MetaBuilder.string().build(); + public static Meta BYTES_META = MetaBuilder.bytes().build(); + + /** + * The type of fields{@link Type} + */ + protected final Type type; + /** + * Name of the meta. + * Optional name, dataSource and version provide a built-in way to indicate what type of data is included. + * Most useful for structs to indicate the semantics of the struct and map it to some existing underlying + * serializer-specific schema. However, can also be useful in specifying other logical types (e.g. a set + * is an array with additional constraints). + */ + protected String name; + /** + * Data source information. + */ + protected String dataSource; + /** + * Version of the meta. + */ + protected Integer version; + /** + * Possible parameters. + */ + protected Map parameters; + + protected Integer hash = null; + + /** + * Base construct. + * + * @param type {@link Type} + * @param name Name of the meta. + * @param version Version of the meta. + * @param dataSource Data source information. + * @param parameters Possible parameters. + */ + public Meta(Type type, String name, Integer version, String dataSource, Map parameters) { + if (null == type) { + throw new RuntimeException("type cannot be null"); + } + this.type = type; + this.name = name; + this.version = version; + this.dataSource = dataSource; + this.parameters = parameters; + } + + /** + * For map type only.Get {@link Meta} information about key. + * + * @return Returns meta information for map key. + */ + public abstract Meta getKeyMeta(); + + /** + * Get {@link Meta} information about value only for map and array types. + * + * @return If the type is map, return the map-value's type; if the type is array, return the list-value's type + */ + public abstract Meta getValueMeta(); + + /** + * For {@link Struct} type only. + * + * @return Returns the List<{@link Field}> corresponding to object[]. + */ + public abstract List getFields(); + + /** + * For {@link Struct} type only. + * + * @param fieldName The name of the field. + * @return Get the field by field name.{@link Field} + */ + public abstract Field getFieldByName(String fieldName); + + /** + * Get the meta's {@link Type}. + * + * @return {@link Type} + */ + public Type getType() { + return this.type; + } + + public String getDataSource() { + return dataSource; + } + + public void setDataSource(String dataSource) { + this.dataSource = dataSource; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public static void validateValue(Meta meta, Object value) { + validateValue(null, meta, value); + } + + public static void validateValue(String name, Meta meta, Object value) { + if (value == null) { + throw new RuntimeException("Invalid value: null used for required field: \"" + name + + "\", meta type: " + meta.getType()); + } + + List expectedClasses = expectedClassesFor(meta); + + if (expectedClasses == null) { + throw new RuntimeException("Invalid Java object for meta type " + meta.getType() + + ": " + value.getClass() + + " for field: \"" + name + "\""); + } + + boolean foundMatch = false; + for (Class expectedClass : expectedClasses) { + if (expectedClass.isInstance(value)) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + throw new RuntimeException("Invalid Java object for meta type " + meta.getType() + + ": " + value.getClass() + + " for field: \"" + name + "\""); + } + + switch (meta.getType()) { + case STRUCT: + Struct struct = (Struct)value; + if (!struct.meta().equals(meta)) { + throw new RuntimeException("Struct metas do not match."); + } + struct.validate(); + break; + case ARRAY: + List array = (List)value; + for (Object entry : array) { + validateValue(meta.getValueMeta(), entry); + } + break; + case MAP: + Map map = (Map)value; + for (Map.Entry entry : map.entrySet()) { + validateValue(meta.getKeyMeta(), entry.getKey()); + validateValue(meta.getValueMeta(), entry.getValue()); + } + break; + } + } + + private static List expectedClassesFor(Meta meta) { + List expectedClasses = LOGICAL_TYPE_CLASSES.get(meta.getName()); + if (expectedClasses == null) { + expectedClasses = META_TYPE_CLASSES.get(meta.getType()); + } + return expectedClasses; + } + + /** + * Validate that the value can be used for this meta, i.e. that its type matches the scmetahema type and optional + * requirements. Throws a DataException if the value is invalid. + * + * @param value the value to validate + */ + public void validateValue(Object value) { + validateValue(this, value); + } + + /** + * Get the {@link Type} associated with the given class. + * + * @param klass the Class to + * @return the corresponding type, or null if there is no matching type + */ + public static Type getMetaType(Class klass) { + synchronized (JAVA_CLASS_META_TYPES) { + Type metaType = JAVA_CLASS_META_TYPES.get(klass); + if (metaType != null) { + return metaType; + } + + // Since the lookup only checks the class, we need to also try + for (Map.Entry, Type> entry : JAVA_CLASS_META_TYPES.entrySet()) { + try { + klass.asSubclass(entry.getKey()); + // Cache this for subsequent lookups + JAVA_CLASS_META_TYPES.put(klass, entry.getValue()); + return entry.getValue(); + } catch (ClassCastException e) { + // Expected, ignore + } + } + } + return null; + } + + public Meta meta() { + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Meta meta = (Meta)o; + return Objects.equals(dataSource, meta.dataSource) && + Objects.equals(name, meta.name) && + Objects.equals(type, meta.type) && + Objects.equals(parameters, meta.parameters); + } + + @Override + public int hashCode() { + if (this.hash == null) { + this.hash = Objects.hash(this.type, this.dataSource, this.name, this.parameters); + } + return this.hash; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Meta{"); + if (this.name != null) { + sb.append("name=").append(this.name).append(", "); + } + if (this.version != null) { + sb.append("version=").append(this.version).append(", "); + } + sb.append("type=").append(this.type).append(", "); + if (this.dataSource != null) { + sb.append("dataSource=").append(this.dataSource).append(", "); + } + sb.append("parameters=").append(this.parameters).append("}"); + return sb.toString(); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaAndData.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaAndData.java new file mode 100644 index 0000000..05560a6 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaAndData.java @@ -0,0 +1,1184 @@ +package io.openmessaging.connector.api.data; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.text.CharacterIterator; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Pattern; + +/** + * Data with meta information. + * Provide a variety of data formatting methods. Easy access to desired data types. + * Provide static methods for data type validation. + * Provide the function of decompiling data strings to MetaAndData. + * + * @author liuboyan + */ +public class MetaAndData { + + private final Meta meta; + private Object data; + + public MetaAndData(Meta meta) { + this.meta = meta; + initData(); + } + + public MetaAndData(Meta meta, Object data) { + this.meta = meta; + if (data != null) { + this.data = data; + } else { + initData(); + } + } + + public Meta getMeta() { + return meta; + } + + public MetaAndData setData(Object data) { + this.data = data; + return this; + } + + public Object getData() { + return data; + } + + private void initData() { + if (this.meta == null || this.meta.getType() == null) { + return; + } + if (this.data == null) { + switch (meta.getType()) { + case STRUCT: + this.data = new Struct(meta); + break; + case ARRAY: + this.data = new ArrayList<>(); + break; + case MAP: + this.data = new HashMap<>(); + break; + default: + break; + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MetaAndData that = (MetaAndData)o; + return Objects.equals(meta, that.meta) && + Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(meta, data); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MetaAndData{").append("meta="); + if (meta != null) { + sb.append(meta); + } + sb.append(", ").append("data="); + if (data != null) { + sb.append(data); + } + sb.append("}"); + return sb.toString(); + } + + /******************************CONSTRUCT********************************/ + + public static MetaAndData getMetaDataFromString(String value) { + return new MetaAndData(Meta.STRING_META).parseString(value); + } + + // base + + public MetaAndData putData(Object data) { + if (this.meta != null) { + if (this.meta.type == Type.ARRAY) { + this.meta.getValueMeta().validateValue(data); + ((List)this.data).add(data); + } else { + this.meta.validateValue(data); + this.data = data; + } + } else { + this.data = data; + } + return this; + } + + // map {"a":15} + + public MetaAndData putData(Object key, Object value) { + if (key == null) { + throw new RuntimeException("key should not be null."); + } + if (this.meta != null) { + this.meta.getKeyMeta().validateValue(key); + this.meta.getValueMeta().validateValue(value); + ((Map)this.data).put(key, value); + } else { + ((Map)this.data).put(key, value); + } + return this; + } + + public MetaAndData putData(Map map) { + if (map == null) { + return this; + } + if (this.meta != null) { + map.forEach((key, value) -> { + this.meta.getKeyMeta().validateValue(key); + this.meta.getValueMeta().validateValue(value); + ((Map)this.data).put(key, value); + }); + } else { + ((Map)this.data).putAll(map); + } + return this; + } + + // array + + public MetaAndData putData(List elements) { + if (elements == null) { + return this; + } + if (this.meta != null) { + elements.forEach(element -> { + this.meta.getValueMeta().validateValue(element); + ((List)this.data).add(element); + }); + } else { + ((List)this.data).addAll(elements); + } + return this; + } + + // struct + + public MetaAndData putData(String fieldName, Object value) { + ((Struct)this.data).put(fieldName, value); + return this; + } + + public MetaAndData putData(Field field, Object value) { + ((Struct)this.data).put(field, value); + return this; + } + + /******************************VALUES********************************/ + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + private static final MetaAndData TRUE_META_AND_VALUE = new MetaAndData(Meta.BOOLEAN_META, Boolean.TRUE); + private static final MetaAndData FALSE_META_AND_VALUE = new MetaAndData(Meta.BOOLEAN_META, Boolean.FALSE); + private static final Meta ARRAY_SELECTOR_META = MetaBuilder.array(Meta.STRING_META).build(); + private static final Meta MAP_SELECTOR_META = MetaBuilder.map(Meta.STRING_META, Meta.STRING_META).build(); + private static final Meta STRUCT_SELECTOR_META = MetaBuilder.struct().build(); + private static final String TRUE_LITERAL = Boolean.TRUE.toString(); + private static final String FALSE_LITERAL = Boolean.FALSE.toString(); + private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; + private static final String NULL_VALUE = "null"; + private static final String ISO_8601_DATE_FORMAT_PATTERN = "yyyy-MM-dd"; + private static final String ISO_8601_TIME_FORMAT_PATTERN = "HH:mm:ss.SSS'Z'"; + private static final String ISO_8601_TIMESTAMP_FORMAT_PATTERN = ISO_8601_DATE_FORMAT_PATTERN + "'T'" + + ISO_8601_TIME_FORMAT_PATTERN; + + private static final String QUOTE_DELIMITER = "\""; + private static final String COMMA_DELIMITER = ","; + private static final String ENTRY_DELIMITER = ":"; + private static final String ARRAY_BEGIN_DELIMITER = "["; + private static final String ARRAY_END_DELIMITER = "]"; + private static final String MAP_BEGIN_DELIMITER = "{"; + private static final String MAP_END_DELIMITER = "}"; + private static final int ISO_8601_DATE_LENGTH = ISO_8601_DATE_FORMAT_PATTERN.length(); + /** + * subtract single quotes + */ + private static final int ISO_8601_TIME_LENGTH = ISO_8601_TIME_FORMAT_PATTERN.length() - 2; + private static final int ISO_8601_TIMESTAMP_LENGTH = ISO_8601_TIMESTAMP_FORMAT_PATTERN.length() - 4; + + private static final Pattern TWO_BACKSLASHES = Pattern.compile("\\\\"); + + private static final Pattern DOUBLEQOUTE = Pattern.compile("\""); + + public Boolean convertToBoolean() throws RuntimeException { + return (Boolean)convertTo(this.meta, Meta.BOOLEAN_META, this.data); + } + + public Byte convertToByte() throws RuntimeException { + return (Byte)convertTo(this.meta, Meta.INT8_META, this.data); + } + + public Short convertToShort() throws RuntimeException { + return (Short)convertTo(this.meta, Meta.INT16_META, this.data); + } + + public Integer convertToInteger() throws RuntimeException { + return (Integer)convertTo(this.meta, Meta.INT32_META, this.data); + } + + public Long convertToLong() throws RuntimeException { + return (Long)convertTo(this.meta, Meta.INT64_META, this.data); + } + + public Float convertToFloat() throws RuntimeException { + return (Float)convertTo(this.meta, Meta.FLOAT32_META, this.data); + } + + public Double convertToDouble() throws RuntimeException { + return (Double)convertTo(this.meta, Meta.FLOAT64_META, this.data); + } + + public String convertToString() { + return (String)convertTo(this.meta, Meta.STRING_META, this.data); + } + + public List convertToList() { + return (List)convertTo(this.meta, ARRAY_SELECTOR_META, this.data); + } + + public Map convertToMap() { + return (Map)convertTo(this.meta, MAP_SELECTOR_META, this.data); + } + + public Struct convertToStruct() { + return (Struct)convertTo(this.meta, STRUCT_SELECTOR_META, this.data); + } + + public java.util.Date convertToTime() { + return (java.util.Date)convertTo(this.meta, Time.META, this.data); + } + + public java.util.Date convertToDate() { + return (java.util.Date)convertTo(this.meta, Date.META, this.data); + } + + public java.util.Date convertToTimestamp() { + return (java.util.Date)convertTo(this.meta, Timestamp.META, this.data); + } + + public BigDecimal convertToDecimal(int scale) { + return (BigDecimal)convertTo(this.meta, Decimal.meta(scale), this.data); + } + + public Meta inferMeta() { + if (this.data == null) { + return null; + } + if (this.data instanceof String) { + return Meta.STRING_META; + } + if (this.data instanceof Boolean) { + return Meta.BOOLEAN_META; + } + if (this.data instanceof Byte) { + return Meta.INT8_META; + } + if (this.data instanceof Short) { + return Meta.INT16_META; + } + if (this.data instanceof Integer) { + return Meta.INT32_META; + } + if (this.data instanceof Long) { + return Meta.INT64_META; + } + if (this.data instanceof Float) { + return Meta.FLOAT32_META; + } + if (this.data instanceof Double) { + return Meta.FLOAT64_META; + } + if (this.data instanceof byte[] || this.data instanceof ByteBuffer) { + return Meta.BYTES_META; + } + if (this.data instanceof List) { + List list = (List)this.data; + if (list.isEmpty()) { + return null; + } + MetaDetector detector = new MetaDetector(); + for (Object element : list) { + if (!detector.canDetect(element)) { + return null; + } + } + return MetaBuilder.array(detector.meta()).build(); + } + if (this.data instanceof Map) { + Map map = (Map)this.data; + if (map.isEmpty()) { + return null; + } + MetaDetector keyDetector = new MetaDetector(); + MetaDetector valueDetector = new MetaDetector(); + for (Map.Entry entry : map.entrySet()) { + if (!keyDetector.canDetect(entry.getKey()) || !valueDetector.canDetect(entry.getValue())) { + return null; + } + } + return MetaBuilder.map(keyDetector.meta(), valueDetector.meta()).build(); + } + if (this.data instanceof Struct) { + return ((Struct)this.data).meta(); + } + return null; + } + + /************************************VALUES_PROTECTED*************************************/ + + /** + * Convert the value to the desired type. + * + * @param fromMeta the meta for the desired type; may not be null + * @param toMeta the meta for the supplied value; may be null if not known + * @return the converted value; never null + * @throws RuntimeException if the value could not be converted to the desired type + */ + protected Object convertTo(Meta fromMeta, Meta toMeta, Object value) throws RuntimeException { + if (value == null) { + throw new RuntimeException("Unable to convert a null value to a meta that requires a value"); + } + if (toMeta == null) { + throw new RuntimeException("Unable to convert a value to a null meta"); + } + switch (toMeta.getType()) { + case BYTES: + if (Decimal.LOGICAL_NAME.equals(toMeta.getName())) { + if (value instanceof ByteBuffer) { + value = toArray((ByteBuffer)value); + } + if (value instanceof byte[]) { + return Decimal.toLogical(toMeta, (byte[])value); + } + if (value instanceof BigDecimal) { + return value; + } + if (value instanceof Number) { + // Not already a decimal, so treat it as a double ... + double converted = ((Number)value).doubleValue(); + return new BigDecimal(converted); + } + if (value instanceof String) { + return new BigDecimal(value.toString()).doubleValue(); + } + } + if (value instanceof ByteBuffer) { + return toArray((ByteBuffer)value); + } + if (value instanceof byte[]) { + return value; + } + if (value instanceof BigDecimal) { + return Decimal.fromLogical(toMeta, (BigDecimal)value); + } + break; + case STRING: + StringBuilder sb = new StringBuilder(); + append(sb, value, false); + return sb.toString(); + case BOOLEAN: + if (value instanceof Boolean) { + return value; + } + if (value instanceof String) { + MetaAndData parsed = parseString(value.toString()); + if (parsed.getData() instanceof Boolean) { + return parsed.getData(); + } + } + return asLong(value, fromMeta, null) == 0L ? Boolean.FALSE : Boolean.TRUE; + case INT8: + if (value instanceof Byte) { + return value; + } + return (byte)asLong(value, fromMeta, null); + case INT16: + if (value instanceof Short) { + return value; + } + return (short)asLong(value, fromMeta, null); + case INT32: + if (Date.LOGICAL_NAME.equals(toMeta.getName())) { + if (value instanceof String) { + MetaAndData parsed = parseString(value.toString()); + value = parsed.getData(); + } + if (value instanceof java.util.Date) { + if (fromMeta != null) { + String fromMetaName = fromMeta.getName(); + if (Date.LOGICAL_NAME.equals(fromMetaName)) { + return value; + } + if (Timestamp.LOGICAL_NAME.equals(fromMetaName)) { + // Just get the number of days from this timestamp + long millis = ((java.util.Date)value).getTime(); + int days = (int)(millis / MILLIS_PER_DAY); // truncates + return Date.toLogical(toMeta, days); + } + } + } + long numeric = asLong(value, fromMeta, null); + return Date.toLogical(toMeta, (int)numeric); + } + if (Time.LOGICAL_NAME.equals(toMeta.getName())) { + if (value instanceof String) { + MetaAndData parsed = parseString(value.toString()); + value = parsed.getData(); + } + if (value instanceof java.util.Date) { + if (fromMeta != null) { + String fromMetaName = fromMeta.getName(); + if (Time.LOGICAL_NAME.equals(fromMetaName)) { + return value; + } + if (Timestamp.LOGICAL_NAME.equals(fromMetaName)) { + // Just get the time portion of this timestamp + Calendar calendar = Calendar.getInstance(UTC); + calendar.setTime((java.util.Date)value); + calendar.set(Calendar.YEAR, 1970); + calendar.set(Calendar.MONTH, 0); // Months are zero-based + calendar.set(Calendar.DAY_OF_MONTH, 1); + return Time.toLogical(toMeta, (int)calendar.getTimeInMillis()); + } + } + } + long numeric = asLong(value, fromMeta, null); + return Time.toLogical(toMeta, (int)numeric); + } + if (value instanceof Integer) { + return value; + } + return (int)asLong(value, fromMeta, null); + case INT64: + if (Timestamp.LOGICAL_NAME.equals(toMeta.getName())) { + if (value instanceof String) { + MetaAndData parsed = parseString(value.toString()); + value = parsed.getData(); + } + if (value instanceof java.util.Date) { + java.util.Date date = (java.util.Date)value; + if (fromMeta != null) { + String fromMetaName = fromMeta.getName(); + if (Date.LOGICAL_NAME.equals(fromMetaName)) { + int days = Date.fromLogical(fromMeta, date); + long millis = days * MILLIS_PER_DAY; + return Timestamp.toLogical(toMeta, millis); + } + if (Time.LOGICAL_NAME.equals(fromMetaName)) { + long millis = Time.fromLogical(fromMeta, date); + return Timestamp.toLogical(toMeta, millis); + } + if (Timestamp.LOGICAL_NAME.equals(fromMetaName)) { + return value; + } + } + } + long numeric = asLong(value, fromMeta, null); + return Timestamp.toLogical(toMeta, numeric); + } + if (value instanceof Long) { + return value; + } + return asLong(value, fromMeta, null); + case FLOAT32: + if (value instanceof Float) { + return value; + } + return (float)asDouble(value, fromMeta, null); + case FLOAT64: + if (value instanceof Double) { + return value; + } + return asDouble(value, fromMeta, null); + case ARRAY: + if (value instanceof String) { + MetaAndData metaAndData = parseString(value.toString()); + value = metaAndData.getData(); + } + if (value instanceof List) { + return value; + } + break; + case MAP: + if (value instanceof String) { + MetaAndData metaAndData = parseString(value.toString()); + value = metaAndData.getData(); + } + if (value instanceof Map) { + return value; + } + break; + case STRUCT: + if (value instanceof Struct) { + Struct struct = (Struct)value; + return struct; + } + if (value instanceof Map) { + Map mapData = (Map)value; + Set entries = mapData.entrySet(); + List fieldMetas = new LinkedList<>(); + List fieldNames = new LinkedList<>(); + List fieldDatas = new LinkedList(); + for (Map.Entry entry : entries) { + String fieldName = entry.getKey().toString(); + MetaAndData valueMeta = parseString( + convertTo(null, Meta.STRING_META, entry.getValue()).toString()); + if (valueMeta.getMeta() == null && valueMeta.getData() instanceof Map) { + valueMeta = new MetaAndData(valueMeta.convertToStruct().meta(), + valueMeta.convertToStruct()); + } + fieldMetas.add(valueMeta.getMeta()); + fieldNames.add(fieldName); + fieldDatas.add(valueMeta.getData()); + } + MetaBuilder structMetaBuilder = MetaBuilder.struct(); + for (int i = 0; i < fieldMetas.size(); i++) { + structMetaBuilder.field(fieldNames.get(i), fieldMetas.get(i)); + } + Meta structMeta = structMetaBuilder.build(); + Struct result = new Struct(structMeta); + for (int i = 0; i < fieldMetas.size(); i++) { + result.put(fieldNames.get(i), fieldDatas.get(i)); + } + return result; + } + } + throw new RuntimeException("Unable to convert " + value + " (" + value.getClass() + ") to " + toMeta); + } + + public MetaAndData parseString(String value) { + if (value == null) { + return null; + } + if (value.isEmpty()) { + return new MetaAndData(Meta.STRING_META, value); + } + Parser parser = new Parser(value); + return parse(parser, false); + } + + protected MetaAndData parse(Parser parser, boolean embedded) throws NoSuchElementException { + if (!parser.hasNext()) { + return null; + } + if (embedded) { + if (parser.canConsume(NULL_VALUE)) { + return null; + } + if (parser.canConsume(QUOTE_DELIMITER)) { + StringBuilder sb = new StringBuilder(); + while (parser.hasNext()) { + if (parser.canConsume(QUOTE_DELIMITER)) { + break; + } + sb.append(parser.next()); + } + return new MetaAndData(Meta.STRING_META, sb.toString()); + } + } + if (parser.canConsume(TRUE_LITERAL)) { + return TRUE_META_AND_VALUE; + } + if (parser.canConsume(FALSE_LITERAL)) { + return FALSE_META_AND_VALUE; + } + int startPosition = parser.mark(); + try { + if (parser.canConsume(ARRAY_BEGIN_DELIMITER)) { + List result = new ArrayList<>(); + Meta elementMeta = null; + while (parser.hasNext()) { + if (parser.canConsume(ARRAY_END_DELIMITER)) { + Meta listMeta = null; + if (elementMeta != null) { + listMeta = MetaBuilder.array(elementMeta).meta(); + } + result = alignListEntriesWithMeta(listMeta, result); + return new MetaAndData(listMeta, result); + } + if (parser.canConsume(COMMA_DELIMITER)) { + throw new RuntimeException("Unable to parse an empty array element: " + parser.original()); + } + MetaAndData element = parse(parser, true); + elementMeta = commonMetaFor(elementMeta, element); + result.add(element.getData()); + parser.canConsume(COMMA_DELIMITER); + } + // Missing either a comma or an end delimiter + if (COMMA_DELIMITER.equals(parser.previous())) { + throw new RuntimeException("Array is missing element after ',': " + parser.original()); + } + throw new RuntimeException("Array is missing terminating ']': " + parser.original()); + } + + if (parser.canConsume(MAP_BEGIN_DELIMITER)) { + Map result = new LinkedHashMap<>(); + Meta keyMeta = null; + Meta valueMeta = null; + boolean objFlag = false; + while (parser.hasNext()) { + if (parser.canConsume(MAP_END_DELIMITER)) { + Meta mapMeta = null; + if (!objFlag) { + mapMeta = MetaBuilder.map(keyMeta, valueMeta).meta(); + } + result = alignMapKeysAndValuesWithMeta(mapMeta, result); + return new MetaAndData(mapMeta, result); + } + if (parser.canConsume(COMMA_DELIMITER)) { + throw new RuntimeException( + "Unable to parse a map entry has no key or value: " + parser.original()); + } + MetaAndData key = parse(parser, true); + if (key == null || key.getData() == null) { + throw new RuntimeException("Map entry may not have a null key: " + parser.original()); + } + if (!parser.canConsume(ENTRY_DELIMITER)) { + throw new RuntimeException("Map entry is missing '=': " + parser.original()); + } + MetaAndData value = parse(parser, true); + Object entryValue = value != null ? value.getData() : null; + result.put(key.getData(), entryValue); + parser.canConsume(COMMA_DELIMITER); + keyMeta = commonMetaFor(keyMeta, key); + valueMeta = commonMetaFor(valueMeta, value); + if (!objFlag && (keyMeta == null || valueMeta == null)) { + objFlag = true; + } + } + // Missing either a comma or an end delimiter + if (COMMA_DELIMITER.equals(parser.previous())) { + throw new RuntimeException("Map is missing element after ',': " + parser.original()); + } + throw new RuntimeException("Map is missing terminating ']': " + parser.original()); + } + } catch (RuntimeException e) { + e.printStackTrace(); + parser.rewindTo(startPosition); + } + String token = parser.next().trim(); + assert !token + .isEmpty(); // original can be empty string but is handled right away; no way for token to be empty here + char firstChar = token.charAt(0); + boolean firstCharIsDigit = Character.isDigit(firstChar); + if (firstCharIsDigit || firstChar == '+' || firstChar == '-') { + try { + // Try to parse as a number ... + BigDecimal decimal = new BigDecimal(token); + try { + return new MetaAndData(Meta.INT8_META, decimal.byteValueExact()); + } catch (ArithmeticException e) { + // continue + } + try { + return new MetaAndData(Meta.INT16_META, decimal.shortValueExact()); + } catch (ArithmeticException e) { + // continue + } + try { + return new MetaAndData(Meta.INT32_META, decimal.intValueExact()); + } catch (ArithmeticException e) { + // continue + } + try { + return new MetaAndData(Meta.INT64_META, decimal.longValueExact()); + } catch (ArithmeticException e) { + // continue + } + double dValue = decimal.doubleValue(); + if (dValue != Double.NEGATIVE_INFINITY && dValue != Double.POSITIVE_INFINITY) { + return new MetaAndData(Meta.FLOAT64_META, dValue); + } + Meta meta = Decimal.meta(decimal.scale()); + return new MetaAndData(meta, decimal); + } catch (NumberFormatException e) { + // can't parse as a number + } + } + if (firstCharIsDigit) { + // Check for a date, time, or timestamp ... + int tokenLength = token.length(); + if (tokenLength == ISO_8601_DATE_LENGTH) { + try { + return new MetaAndData(Date.META, new SimpleDateFormat(ISO_8601_DATE_FORMAT_PATTERN).parse(token)); + } catch (ParseException e) { + // not a valid date + } + } else if (tokenLength == ISO_8601_TIME_LENGTH) { + try { + return new MetaAndData(Time.META, new SimpleDateFormat(ISO_8601_TIME_FORMAT_PATTERN).parse(token)); + } catch (ParseException e) { + // not a valid date + } + } else if (tokenLength == ISO_8601_TIMESTAMP_LENGTH) { + try { + return new MetaAndData(Timestamp.META, + new SimpleDateFormat(ISO_8601_TIMESTAMP_FORMAT_PATTERN).parse(token)); + } catch (ParseException e) { + // not a valid date + } + } + } + // At this point, the only thing this can be is a string. Embedded strings were processed above, + // so this is not embedded and we can use the original string... + return new MetaAndData(Meta.STRING_META, parser.original()); + } + + protected List alignListEntriesWithMeta(Meta meta, List input) { + if (meta == null) { + return input; + } + Meta valueMeta = meta.getValueMeta(); + List result = new ArrayList<>(); + for (Object value : input) { + Object newValue = convertTo(null, valueMeta, value); + result.add(newValue); + } + return result; + } + + protected Map alignMapKeysAndValuesWithMeta(Meta mapMeta, Map input) { + if (mapMeta == null) { + return input; + } + Meta keyMeta = mapMeta.getKeyMeta(); + Meta valueMeta = mapMeta.getValueMeta(); + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : input.entrySet()) { + Object newKey = convertTo(keyMeta, null, entry.getKey()); + Object newValue = convertTo(valueMeta, null, entry.getValue()); + result.put(newKey, newValue); + } + return result; + } + + protected Meta commonMetaFor(Meta previous, MetaAndData latest) { + if (latest == null) { + return previous; + } + if (previous == null) { + return latest.getMeta(); + } + Meta newMeta = latest.getMeta(); + Type previousType = previous.getType(); + Type newType = newMeta.getType(); + if (previousType != newType) { + switch (previous.getType()) { + case INT8: + if (newType == Type.INT16 || newType == Type.INT32 || newType == Type.INT64 + || newType == Type.FLOAT32 || newType == + Type.FLOAT64) { + return newMeta; + } + break; + case INT16: + if (newType == Type.INT8) { + return previous; + } + if (newType == Type.INT32 || newType == Type.INT64 || newType == Type.FLOAT32 + || newType == Type.FLOAT64) { + return newMeta; + } + break; + case INT32: + if (newType == Type.INT8 || newType == Type.INT16) { + return previous; + } + if (newType == Type.INT64 || newType == Type.FLOAT32 || newType == Type.FLOAT64) { + return newMeta; + } + break; + case INT64: + if (newType == Type.INT8 || newType == Type.INT16 || newType == Type.INT32) { + return previous; + } + if (newType == Type.FLOAT32 || newType == Type.FLOAT64) { + return newMeta; + } + break; + case FLOAT32: + if (newType == Type.INT8 || newType == Type.INT16 || newType == Type.INT32 + || newType == Type.INT64) { + return previous; + } + if (newType == Type.FLOAT64) { + return newMeta; + } + break; + case FLOAT64: + if (newType == Type.INT8 || newType == Type.INT16 || newType == Type.INT32 || newType == Type.INT64 + || newType == + Type.FLOAT32) { + return previous; + } + break; + } + return null; + } + if (!previous.equals(newMeta)) { + return null; + } + return previous; + } + + protected void append(StringBuilder sb, Object value, boolean embedded) { + if (value == null) { + sb.append(NULL_VALUE); + } else if (value instanceof Number) { + sb.append(value); + } else if (value instanceof Boolean) { + sb.append(value); + } else if (value instanceof String) { + if (embedded) { + String escaped = escape((String)value); + sb.append('"').append(escaped).append('"'); + } else { + sb.append(value); + } + } else if (value instanceof byte[]) { + value = Base64.getEncoder().encodeToString((byte[])value); + if (embedded) { + sb.append('"').append(value).append('"'); + } else { + sb.append(value); + } + } else if (value instanceof ByteBuffer) { + byte[] bytes = readBytes((ByteBuffer)value); + append(sb, bytes, embedded); + } else if (value instanceof List) { + List list = (List)value; + sb.append('['); + appendIterable(sb, list.iterator()); + sb.append(']'); + } else if (value instanceof Map) { + Map map = (Map)value; + sb.append('{'); + appendIterable(sb, map.entrySet().iterator()); + sb.append('}'); + } else if (value instanceof Struct) { + Struct struct = (Struct)value; + Meta meta = struct.meta(); + boolean first = true; + sb.append('{'); + for (Field field : meta.getFields()) { + if (first) { + first = false; + } else { + sb.append(','); + } + append(sb, field.name(), true); + sb.append(':'); + append(sb, struct.get(field), true); + } + sb.append('}'); + } else if (value instanceof Map.Entry) { + Map.Entry entry = (Map.Entry)value; + append(sb, entry.getKey(), true); + sb.append(':'); + append(sb, entry.getValue(), true); + } else if (value instanceof java.util.Date) { + java.util.Date dateValue = (java.util.Date)value; + String formatted = dateFormatFor(dateValue).format(dateValue); + sb.append(formatted); + } else { + throw new RuntimeException( + "Failed to serialize unexpected value type " + value.getClass().getName() + ": " + value); + } + } + + protected String escape(String value) { + String replace1 = TWO_BACKSLASHES.matcher(value).replaceAll("\\\\\\\\"); + return DOUBLEQOUTE.matcher(replace1).replaceAll("\\\\\""); + } + + protected void appendIterable(StringBuilder sb, Iterator iter) { + if (iter.hasNext()) { + append(sb, iter.next(), true); + while (iter.hasNext()) { + sb.append(','); + append(sb, iter.next(), true); + } + } + } + + protected double asDouble(Object value, Meta meta, Throwable error) { + try { + if (value instanceof Number) { + Number number = (Number)value; + return number.doubleValue(); + } + if (value instanceof String) { + return new BigDecimal(value.toString()).doubleValue(); + } + } catch (NumberFormatException e) { + error = e; + // fall through + } + return asLong(value, meta, error); + } + + protected long asLong(Object value, Meta fromMeta, Throwable error) { + try { + if (value instanceof Number) { + Number number = (Number)value; + return number.longValue(); + } + if (value instanceof String) { + return new BigDecimal(value.toString()).longValue(); + } + } catch (NumberFormatException e) { + error = e; + // fall through + } + if (fromMeta != null) { + String metaName = fromMeta.getName(); + if (value instanceof java.util.Date) { + if (Date.LOGICAL_NAME.equals(metaName)) { + return Date.fromLogical(fromMeta, (java.util.Date)value); + } + if (Time.LOGICAL_NAME.equals(metaName)) { + return Time.fromLogical(fromMeta, (java.util.Date)value); + } + if (Timestamp.LOGICAL_NAME.equals(metaName)) { + return Timestamp.fromLogical(fromMeta, (java.util.Date)value); + } + } + throw new RuntimeException("Unable to convert " + value + " (" + value.getClass() + ") to " + fromMeta, + error); + } + throw new RuntimeException("Unable to convert " + value + " (" + value.getClass() + ") to a number", error); + } + + /**************************UTILS****************************/ + + private DateFormat dateFormatFor(java.util.Date value) { + if (value.getTime() < MILLIS_PER_DAY) { + return new SimpleDateFormat(ISO_8601_TIME_FORMAT_PATTERN); + } + if (value.getTime() % MILLIS_PER_DAY == 0) { + return new SimpleDateFormat(ISO_8601_DATE_FORMAT_PATTERN); + } + return new SimpleDateFormat(ISO_8601_TIMESTAMP_FORMAT_PATTERN); + } + + private byte[] toArray(ByteBuffer buffer) { + return toArray(buffer, 0, buffer.remaining()); + } + + private byte[] toArray(ByteBuffer buffer, int offset, int size) { + byte[] dest = new byte[size]; + if (buffer.hasArray()) { + System.arraycopy(buffer.array(), buffer.position() + buffer.arrayOffset() + offset, dest, 0, size); + } else { + int pos = buffer.position(); + buffer.position(pos + offset); + buffer.get(dest); + buffer.position(pos); + } + return dest; + } + + /** + * Read a buffer into a Byte array for the given offset and length + */ + private byte[] readBytes(ByteBuffer buffer, int offset, int length) { + byte[] dest = new byte[length]; + if (buffer.hasArray()) { + System.arraycopy(buffer.array(), buffer.arrayOffset() + offset, dest, 0, length); + } else { + buffer.mark(); + buffer.position(offset); + buffer.get(dest, 0, length); + buffer.reset(); + } + return dest; + } + + /** + * Read the given byte buffer into a Byte array + */ + private byte[] readBytes(ByteBuffer buffer) { + return readBytes(buffer, 0, buffer.limit()); + } + + protected class MetaDetector { + private Type knownType = null; + + public MetaDetector() { + } + + public boolean canDetect(Object value) { + if (value == null) { + return true; + } + Meta schema = inferMeta(); + if (schema == null) { + return false; + } + if (knownType == null) { + knownType = schema.getType(); + } else if (knownType != schema.getType()) { + return false; + } + return true; + } + + public Meta meta() { + return MetaBuilder.type(knownType).meta(); + } + } + + protected class Parser { + private final String original; + private final CharacterIterator iter; + private String nextToken = null; + private String previousToken = null; + + public Parser(String original) { + this.original = original; + this.iter = new StringCharacterIterator(this.original); + } + + public int position() { + return iter.getIndex(); + } + + public int mark() { + return iter.getIndex() - (nextToken != null ? nextToken.length() : 0); + } + + public void rewindTo(int position) { + iter.setIndex(position); + nextToken = null; + } + + public String original() { + return original; + } + + public boolean hasNext() { + return nextToken != null || canConsumeNextToken(); + } + + protected boolean canConsumeNextToken() { + return iter.getEndIndex() > iter.getIndex(); + } + + public String next() { + if (nextToken != null) { + previousToken = nextToken; + nextToken = null; + } else { + previousToken = consumeNextToken(); + } + return previousToken; + } + + private String consumeNextToken() throws NoSuchElementException { + boolean escaped = false; + int start = iter.getIndex(); + char c = iter.current(); + while (c != CharacterIterator.DONE) { + switch (c) { + case '\\': + escaped = !escaped; + break; + case ':': + case ',': + case '{': + case '}': + case '[': + case ']': + case '\"': + if (!escaped) { + if (start < iter.getIndex()) { + // Return the previous token + return original.substring(start, iter.getIndex()); + } + // Consume and return this delimiter as a token + iter.next(); + return original.substring(start, start + 1); + } + // escaped, so continue + escaped = false; + break; + default: + // If escaped, then we don't care what was escaped + escaped = false; + break; + } + c = iter.next(); + } + return original.substring(start, iter.getIndex()); + } + + public String previous() { + return previousToken; + } + + public boolean canConsume(String expected) { + return canConsume(expected, true); + } + + public boolean canConsume(String expected, boolean ignoreLeadingAndTrailingWhitespace) { + if (isNext(expected, ignoreLeadingAndTrailingWhitespace)) { + // consume this token ... + nextToken = null; + return true; + } + return false; + } + + protected boolean isNext(String expected, boolean ignoreLeadingAndTrailingWhitespace) { + if (nextToken == null) { + if (!hasNext()) { + return false; + } + // There's another token, so consume it + nextToken = consumeNextToken(); + } + if (ignoreLeadingAndTrailingWhitespace) { + nextToken = nextToken.trim(); + while (nextToken.isEmpty() && canConsumeNextToken()) { + nextToken = consumeNextToken().trim(); + } + } + return nextToken.equals(expected); + } + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaArray.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaArray.java new file mode 100644 index 0000000..f051d29 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaArray.java @@ -0,0 +1,95 @@ +package io.openmessaging.connector.api.data; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Meta implement for type of ARRAY. + * + * @author liuboyan + */ +public class MetaArray extends Meta { + private final Meta valueMeta; + + /** + * Base construct. + * + * @param type {@link Type} + * @param version Version of the meta. + * @param name Name of the meta. + * @param dataSource Data source information. + * @param parameters Possible parameters. + */ + public MetaArray(Type type, String name, Integer version, String dataSource, Map parameters, Meta valueMeta) { + super(type, name, version, dataSource, parameters); + if (type != Type.ARRAY) { + throw new RuntimeException("type should be ARRAY."); + } + this.valueMeta = valueMeta; + } + + @Override + public Meta getKeyMeta() { + throw new RuntimeException("Cannot look up key meta on non-map type"); + } + + @Override + public Meta getValueMeta() { + return this.valueMeta; + } + + @Override + public List getFields() { + throw new RuntimeException("Cannot list fields on non-struct type"); + } + + @Override + public Field getFieldByName(String fieldName) { + throw new RuntimeException("Cannot look up fields on non-struct type"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + MetaArray meta = (MetaArray)o; + return Objects.equals(this.valueMeta, meta.valueMeta); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + this.valueMeta.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MetaArray{"); + if (this.name != null) { + sb.append("name=").append(this.name).append(", "); + } + if (this.version != null) { + sb.append("version=").append(this.version).append(", "); + } + sb.append("type=").append(this.type).append(", "); + if (this.dataSource != null) { + sb.append("dataSource=").append(this.dataSource).append(", "); + } + if (this.valueMeta != null) { + sb.append("valueMeta=").append(this.valueMeta).append(", "); + } + sb.append("parameters=").append(this.parameters).append("}"); + return sb.toString(); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaArrayBuilder.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaArrayBuilder.java new file mode 100644 index 0000000..1099383 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaArrayBuilder.java @@ -0,0 +1,55 @@ +package io.openmessaging.connector.api.data; + +import java.util.Collections; +import java.util.List; + +/** + * MetaBuilder implement for type of ARRAY. + * + * @author liuboyan + */ +public class MetaArrayBuilder extends MetaBuilder { + private Meta valueMeta; + + public MetaArrayBuilder(Type type, Meta valueMeta) { + super(type); + this.valueMeta = valueMeta; + } + + @Override + public MetaBuilder field(String fieldName, Meta fieldMeta) { + throw new RuntimeException("Cannot create fields on type " + this.type()); + } + + @Override + public MetaBuilder field(Field field) { + throw new RuntimeException("Cannot create fields on type " + this.type()); + } + + @Override + public Meta keyMeta() { + return null; + } + + @Override + public Meta valueMeta() { + return this.valueMeta; + } + + @Override + public List fields() { + throw new RuntimeException("Cannot list fields on non-struct type"); + } + + @Override + public Field getFieldByName(String fieldName) { + throw new RuntimeException("Cannot look up fields on non-struct type"); + } + + @Override + public Meta build() { + return new MetaArray(this.type(), this.name(), this.version(), + this.dataSource(), this.parameters() == null ? null : Collections.unmodifiableMap(this.parameters()), + this.valueMeta); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaBase.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaBase.java new file mode 100644 index 0000000..49bdd39 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaBase.java @@ -0,0 +1,75 @@ +package io.openmessaging.connector.api.data; + +import java.util.List; +import java.util.Map; + +/** + * Meta implement for base types. + * {@link Type}.INT8 {@link Type}.INT16 {@link Type}.INT32 {@link Type}.INT64 + * {@link Type}.BIG_INTEGER {@link Type}.FLOAT32 {@link Type}.FLOAT64 {@link Type}.BOOLEAN + * {@link Type}.STRING {@link Type}.BYTES {@link Type}.DATETIME + * + * @author liuboyan + */ +public class MetaBase extends Meta { + /** + * Base construct. + * + * @param type {@link Type} + * @param name Name of the meta. + * @param version Version of the meta. + * @param dataSource Data source information. + * @param parameters Possible parameters. + */ + public MetaBase(Type type, String name, Integer version, String dataSource, Map parameters) { + super(type, name, version, dataSource, parameters); + } + + @Override + public Meta getKeyMeta() { + throw new RuntimeException("Cannot look up key meta on non-map type"); + } + + @Override + public Meta getValueMeta() { + throw new RuntimeException("Cannot look up value meta on non-map type"); + } + + @Override + public List getFields() { + throw new RuntimeException("Cannot list fields on non-struct type"); + } + + @Override + public Field getFieldByName(String fieldName) { + throw new RuntimeException("Cannot look up fields on non-struct type"); + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MetaBase{"); + if (this.name != null) { + sb.append("name=").append(this.name).append(", "); + } + if (this.version != null) { + sb.append("version=").append(this.version).append(", "); + } + sb.append("type=").append(this.type).append(", "); + if (this.dataSource != null) { + sb.append("dataSource=").append(this.dataSource).append(", "); + } + sb.append("parameters=").append(this.parameters).append("}"); + return sb.toString(); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaBaseBuilder.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaBaseBuilder.java new file mode 100644 index 0000000..8d0adb7 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaBaseBuilder.java @@ -0,0 +1,54 @@ +package io.openmessaging.connector.api.data; + +import java.util.Collections; +import java.util.List; + +/** + * MetaBuilder implement for base types. + * {@link Type}.INT8 {@link Type}.INT16 {@link Type}.INT32 {@link Type}.INT64 + * {@link Type}.BIG_INTEGER {@link Type}.FLOAT32 {@link Type}.FLOAT64 {@link Type}.BOOLEAN + * {@link Type}.STRING {@link Type}.BYTES {@link Type}.DATETIME + * + * @author liuboyan + */ +public class MetaBaseBuilder extends MetaBuilder { + public MetaBaseBuilder(Type type) { + super(type); + } + + @Override + public MetaBuilder field(String fieldName, Meta fieldMeta) { + return this; + } + + @Override + public MetaBuilder field(Field field) { + return this; + } + + @Override + public Meta keyMeta() { + return null; + } + + @Override + public Meta valueMeta() { + return null; + } + + @Override + public List fields() { + return null; + } + + @Override + public Field getFieldByName(String fieldName) { + return null; + } + + @Override + public Meta build() { + return new MetaBase(this.type(), this.name(), this.version(), + this.dataSource(), this.parameters() == null ? null : Collections.unmodifiableMap(this.parameters())); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaBuilder.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaBuilder.java new file mode 100644 index 0000000..14be2cf --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaBuilder.java @@ -0,0 +1,269 @@ +package io.openmessaging.connector.api.data; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + *

+ * MetaBuilder provides a fluent API for constructing {@link Meta} objects. It allows you to set each of the + * properties for the meta and each call returns the MetaBuilder so the calls can be chained. When nested + * types are required, use one of the predefined metas from {@link Meta} or use a second MetaBuilder inline. + *

+ *

+ * Here is an example of building a struct meta: + *

+ *     Meta dateMeta = MetaBuilder.struct()
+ *         .name("com.example.CalendarDate").version(2)
+ *         .field("month", Meta.STRING_META)
+ *         .field("day", Meta.INT8_META)
+ *         .field("year", Meta.INT16_META)
+ *         .build();
+ *     
+ *

+ *

+ * Here is an example of using a second MetaBuilder to construct complex, nested types: + *

+ *     Meta userListMeta = MetaBuilder.array(
+ *         MetaBuilder.struct().name("com.example.User").field("username", Meta.STRING_META).field("id", Meta
+ *         .INT64_META).build()
+ *     ).build();
+ *     
+ *

+ * + * @author liuboyan + */ +public abstract class MetaBuilder { + + private static final String TYPE_FIELD = "type"; + private static final String NAME_FIELD = "name"; + private static final String DATASOURCE = "dataSource"; + + private final Type type; + private String name; + private Integer version; + private String dataSource; + private Map parameters; + + public MetaBuilder(Type type) { + if (null == type) { + throw new RuntimeException("type cannot be null"); + } + this.type = type; + } + + public Integer version() { + return this.version; + } + + public MetaBuilder version(Integer version) { + this.version = version; + return this; + } + + public String name() { + return this.name; + } + + public MetaBuilder name(String name) { + checkCanSet(NAME_FIELD, this.name, name); + this.name = name; + return this; + } + + public String dataSource() { + return this.dataSource; + } + + public MetaBuilder dataSource(String dataSource) { + checkCanSet(DATASOURCE, this.dataSource, dataSource); + this.dataSource = dataSource; + return this; + } + + public Map parameters() { + return this.parameters == null ? null : Collections.unmodifiableMap(this.parameters); + } + + public MetaBuilder parameter(String propertyName, String propertyValue) { + // Preserve order of insertion with a LinkedHashMap. This isn't strictly necessary, but is nice if logical types + // can print their properties in a consistent order. + if (this.parameters == null) { + this.parameters = new LinkedHashMap<>(); + } + this.parameters.put(propertyName, propertyValue); + return this; + } + + public MetaBuilder parameters(Map props) { + // Avoid creating an empty set of properties so we never have an empty map + if (props.isEmpty()) { + return this; + } + if (this.parameters == null) { + this.parameters = new LinkedHashMap<>(); + } + this.parameters.putAll(props); + return this; + } + + public Type type() { + return this.type; + } + + public static MetaBuilder type(Type type) { + switch (type) { + case MAP: + return new MetaMapBuilder(type, null, null); + case ARRAY: + return new MetaArrayBuilder(type, null); + case STRUCT: + return new MetaStructBuilder(type); + default: + return new MetaBaseBuilder(type); + } + } + + public abstract MetaBuilder field(String fieldName, Meta fieldMeta); + + public abstract MetaBuilder field(Field field); + + public abstract Meta keyMeta(); + + public abstract Meta valueMeta(); + + public abstract List fields(); + + public abstract Field getFieldByName(String fieldName); + + public abstract Meta build(); + + /** + * Return a concrete instance of the {@link Meta} specified by this builder + * + * @return the {@link Meta} + */ + public Meta meta() { + return build(); + } + + private static void checkCanSet(String fieldName, Object fieldVal, Object val) { + if (fieldVal != null && fieldVal != val) { + throw new RuntimeException("Invalid MetaBuilder call: " + fieldName + " has already been set."); + } + } + + private static void checkNotNull(String fieldName, Object val, String fieldToSet) { + if (val == null) { + throw new RuntimeException( + "Invalid MetaBuilder call: " + fieldName + " must be specified to set " + fieldToSet); + } + } + + // Basic types + + /** + * @return a new {@link Type#INT8} MetaBuilder + */ + public static MetaBuilder int8() { + return new MetaBaseBuilder(Type.INT8); + } + + /** + * @return a new {@link Type#INT16} MetaBuilder + */ + public static MetaBuilder int16() { + return new MetaBaseBuilder(Type.INT16); + } + + /** + * @return a new {@link Type#INT32} MetaBuilder + */ + public static MetaBuilder int32() { + return new MetaBaseBuilder(Type.INT32); + } + + /** + * @return a new {@link Type#INT64} MetaBuilder + */ + public static MetaBuilder int64() { + return new MetaBaseBuilder(Type.INT64); + } + + /** + * @return a new {@link Type#FLOAT32} MetaBuilder + */ + public static MetaBuilder float32() { + return new MetaBaseBuilder(Type.FLOAT32); + } + + /** + * @return a new {@link Type#FLOAT64} MetaBuilder + */ + public static MetaBuilder float64() { + return new MetaBaseBuilder(Type.FLOAT64); + } + + /** + * @return a new {@link Type#BOOLEAN} MetaBuilder + */ + public static MetaBuilder bool() { + return new MetaBaseBuilder(Type.BOOLEAN); + } + + /** + * @return a new {@link Type#STRING} MetaBuilder + */ + public static MetaBuilder string() { + return new MetaBaseBuilder(Type.STRING); + } + + /** + * @return a new {@link Type#BYTES} MetaBuilder + */ + public static MetaBuilder bytes() { + return new MetaBaseBuilder(Type.BYTES); + } + + // Structs + + /** + * @return a new {@link Type#STRUCT} MetaBuilder + */ + public static MetaBuilder struct() { + return new MetaStructBuilder(Type.STRUCT); + } + + // Arrays + + /** + * @param valueMeta the meta for elements of the array + * @return a new {@link Type#ARRAY} MetaBuilder + */ + public static MetaBuilder array(Meta valueMeta) { + if (null == valueMeta) { + throw new RuntimeException("valueMeta cannot be null."); + } + MetaBuilder builder = new MetaArrayBuilder(Type.ARRAY, valueMeta); + return builder; + } + + // Maps + + /** + * @param keyMeta the meta for keys in the map + * @param valueMeta the meta for values in the map + * @return a new {@link Type#MAP} MetaBuilder + */ + public static MetaBuilder map(Meta keyMeta, Meta valueMeta) { + if (null == keyMeta) { + throw new RuntimeException("keyMeta cannot be null."); + } + if (null == valueMeta) { + throw new RuntimeException("valueMeta cannot be null."); + } + MetaBuilder builder = new MetaMapBuilder(Type.MAP, keyMeta, valueMeta); + return builder; + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaMap.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaMap.java new file mode 100644 index 0000000..928e0f9 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaMap.java @@ -0,0 +1,103 @@ +package io.openmessaging.connector.api.data; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Meta implement for type of MAP. + * + * @author liuboyan + */ +public class MetaMap extends Meta { + private final Meta keyMeta; + private final Meta valueMeta; + + /** + * Base construct. + * + * @param type {@link Type} + * @param name Name of the meta. + * @param version Version of the meta. + * @param dataSource Data source information. + * @param parameters Possible parameters. + */ + public MetaMap(Type type, String name, Integer version, String dataSource, Map parameters, Meta keyMeta, Meta valueMeta) { + super(type, name, version, dataSource, parameters); + if(type != Type.MAP){ + throw new RuntimeException("type should be MAP."); + } + this.keyMeta = keyMeta; + this.valueMeta = valueMeta; + } + + @Override + public Meta getKeyMeta() { + return this.keyMeta; + } + + @Override + public Meta getValueMeta() { + return this.valueMeta; + } + + @Override + public List getFields() { + throw new RuntimeException("Cannot list fields on non-struct type"); + } + + @Override + public Field getFieldByName(String fieldName) { + throw new RuntimeException("Cannot look up fields on non-struct type"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + MetaMap meta = (MetaMap) o; + return Objects.equals(this.keyMeta, meta.keyMeta) + && Objects.equals(this.valueMeta, meta.valueMeta); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + this.keyMeta.hashCode(); + result = 31 * result + this.valueMeta.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MetaMap{"); + if(this.name != null){ + sb.append("name=").append(this.name).append(", "); + } + if (this.version != null) { + sb.append("version=").append(this.version).append(", "); + } + sb.append("type=").append(this.type).append(", "); + if(this.dataSource != null){ + sb.append("dataSource=").append(this.dataSource).append(", "); + } + if(this.keyMeta!=null){ + sb.append("keyMeta=").append(this.keyMeta).append(", "); + } + if(this.valueMeta!=null){ + sb.append("valueMeta=").append(this.valueMeta).append(", "); + } + sb.append("parameters=").append(this.parameters).append("}"); + return sb.toString(); + } + +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaMapBuilder.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaMapBuilder.java new file mode 100644 index 0000000..9732f0b --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaMapBuilder.java @@ -0,0 +1,57 @@ +package io.openmessaging.connector.api.data; + +import java.util.Collections; +import java.util.List; + +/** + * MetaBuilder implement for type of MAP. + * + * @author liuboyan + */ +public class MetaMapBuilder extends MetaBuilder { + private Meta keyMeta; + private Meta valueMeta; + + public MetaMapBuilder(Type type, Meta keyMeta, Meta valueMeta) { + super(type); + this.keyMeta = keyMeta; + this.valueMeta = valueMeta; + } + + @Override + public MetaBuilder field(String fieldName, Meta fieldMeta) { + throw new RuntimeException("Cannot create fields on type " + this.type()); + } + + @Override + public MetaBuilder field(Field field) { + throw new RuntimeException("Cannot create fields on type " + this.type()); + } + + @Override + public Meta keyMeta() { + return this.keyMeta; + } + + @Override + public Meta valueMeta() { + return this.valueMeta; + } + + @Override + public List fields() { + throw new RuntimeException("Cannot list fields on non-struct type"); + } + + @Override + public Field getFieldByName(String fieldName) { + throw new RuntimeException("Cannot look up fields on non-struct type"); + } + + @Override + public Meta build() { + return new MetaMap(this.type(), this.name(), this.version(), + this.dataSource(), this.parameters() == null ? null : Collections.unmodifiableMap(this.parameters()), + this.keyMeta, this.valueMeta); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaStruct.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaStruct.java new file mode 100644 index 0000000..00ad2e7 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaStruct.java @@ -0,0 +1,110 @@ +package io.openmessaging.connector.api.data; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Meta implement for type of STRUCT. + * + * @author liuboyan + */ +public class MetaStruct extends Meta { + + /** + * Structure of the meta, contains a list of {@link Field} + */ + private List fields; + /** + * field name 和 field的对应map,便于快速查找 + */ + private Map fieldsByName; + + /** + * Base construct. + * + * @param type {@link Type} + * @param name Name of the meta. + * @param version Version of the meta. + * @param dataSource Data source information. + * @param parameters Possible parameters. + */ + public MetaStruct(Type type, String name, Integer version, String dataSource, Map parameters, List fields) { + super(type, name, version, dataSource, parameters); + if(type != Type.STRUCT){ + throw new RuntimeException("type should be STRUCT."); + } + this.fields = fields == null ? Collections.emptyList() : fields; + this.fieldsByName = new HashMap<>(this.fields.size()); + for (Field field : this.fields) { + fieldsByName.put(field.name(), field); + } + } + + + @Override + public Meta getKeyMeta() { + throw new RuntimeException("Cannot look up key meta on non-map type"); + } + + @Override + public Meta getValueMeta() { + throw new RuntimeException("Cannot look up value meta on non-map type"); + } + + @Override + public List getFields() { + return this.fields; + } + + @Override + public Field getFieldByName(String fieldName) { + return this.fieldsByName.get(fieldName); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + MetaStruct meta = (MetaStruct) o; + return Objects.equals(this.fields, meta.fields); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + this.fields.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MetaStruct{"); + if(this.name != null){ + sb.append("name=").append(this.name).append(", "); + } + if (this.version != null) { + sb.append("version=").append(this.version).append(", "); + } + sb.append("type=").append(this.type).append(", "); + if(this.dataSource != null){ + sb.append("dataSource=").append(this.dataSource).append(", "); + } + if(this.fields!=null){ + sb.append("fields=").append(this.fields).append(", "); + } + sb.append("parameters=").append(this.parameters).append("}"); + return sb.toString(); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/MetaStructBuilder.java b/connector/src/main/java/io/openmessaging/connector/api/data/MetaStructBuilder.java new file mode 100644 index 0000000..4c1d865 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/MetaStructBuilder.java @@ -0,0 +1,72 @@ +package io.openmessaging.connector.api.data; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * MetaBuilder implement for type of STRUCT. + * + * @author liuboyan + */ +public class MetaStructBuilder extends MetaBuilder { + private Map fields; + + public MetaStructBuilder(Type type) { + super(type); + this.fields = new LinkedHashMap<>(); + } + + @Override + public MetaBuilder field(String fieldName, Meta fieldMeta) { + if (null == fieldName || fieldName.isEmpty()) { + throw new RuntimeException("fieldName cannot be null."); + } + if (null == fieldMeta) { + throw new RuntimeException("fieldMeta for field " + fieldName + " cannot be null."); + } + int fieldIndex = this.fields.size(); + if (this.fields.containsKey(fieldName)) { + throw new RuntimeException("Cannot create field because of field name duplication " + fieldName); + } + this.fields.put(fieldName, new Field(fieldIndex, fieldName, fieldMeta)); + return this; + } + + @Override + public MetaBuilder field(Field field) { + return this.field(field.name(), field.meta()); + } + + @Override + public Meta keyMeta() { + return null; + } + + @Override + public Meta valueMeta() { + return null; + } + + @Override + public List fields() { + return new ArrayList<>(this.fields.values()); + } + + @Override + public Field getFieldByName(String fieldName) { + return fields.get(fieldName); + } + + @Override + public Meta build() { + return new MetaStruct(this.type(), + this.name(), + this.version(), + this.dataSource(), + this.parameters() == null ? null : Collections.unmodifiableMap(this.parameters()), + fields == null ? null : Collections.unmodifiableList(new ArrayList<>(fields.values()))); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Schema.java b/connector/src/main/java/io/openmessaging/connector/api/data/Schema.java deleted file mode 100644 index 70b7921..0000000 --- a/connector/src/main/java/io/openmessaging/connector/api/data/Schema.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.openmessaging.connector.api.data; - -import java.util.List; -import java.util.Objects; - -/** - * Schema - * - * @version OMS 0.1.0 - * @since OMS 0.1.0 - */ -public class Schema { - - /** - * Data source information. - */ - private String dataSource; - /** - * Name of the schema. - */ - private String name; - /** - * Structure of the schema, contains a list of {@link Field} - */ - private List fields; - - public String getDataSource() { - return dataSource; - } - - public void setDataSource(String dataSource) { - this.dataSource = dataSource; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getFields() { - return fields; - } - - public void setFields(List fields) { - this.fields = fields; - } - - public Field getField(String fieldName) { - - for (Field field : fields) { - if (field.getName().equals(fieldName)) { - return field; - } - } - return null; - } - - @Override public String toString() { - return "Schema{" + - "dataSource='" + dataSource + '\'' + - ", name='" + name + '\'' + - ", fields=" + fields + - '}'; - } - - @Override public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof Schema)) - return false; - Schema schema = (Schema) o; - return Objects.equals(dataSource, schema.dataSource) && - Objects.equals(name, schema.name) && - Objects.equals(fields, schema.fields); - } - - @Override public int hashCode() { - return Objects.hash(dataSource, name, fields); - } -} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/SinkDataEntry.java b/connector/src/main/java/io/openmessaging/connector/api/data/SinkDataEntry.java index df46ed2..fccf483 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/data/SinkDataEntry.java +++ b/connector/src/main/java/io/openmessaging/connector/api/data/SinkDataEntry.java @@ -17,7 +17,7 @@ package io.openmessaging.connector.api.data; -import java.util.Objects; +import io.openmessaging.connector.api.header.Headers; /** * SinkDataEntry is read from message queue and includes the queueOffset of the data in message queue. @@ -26,58 +26,101 @@ * @since OMS 0.1.0 */ public class SinkDataEntry extends DataEntry { + /** + * Offset in the message queue. + */ + private Long queueOffset; public SinkDataEntry(Long queueOffset, Long timestamp, + String queueName, + String shardingKey, EntryType entryType, + MetaAndData key, + MetaAndData value, + Headers headers) { + super(timestamp, queueName, shardingKey, entryType, key, value, headers); + this.queueOffset = queueOffset; + } + + public SinkDataEntry(Long queueOffset, String queueName, - Schema schema, - Object[] payload) { - this(queueOffset, timestamp, entryType, queueName, schema, null, payload); + String shardingKey, + EntryType entryType, + MetaAndData key, + MetaAndData value, + Headers headers) { + this(queueOffset, null, queueName, shardingKey, entryType, key, value, headers); } public SinkDataEntry(Long queueOffset, Long timestamp, + String queueName, + String shardingKey, EntryType entryType, + MetaAndData key, + MetaAndData value) { + this(queueOffset, timestamp, queueName, shardingKey, entryType, key, value, null); + } + + public SinkDataEntry newRecord(Long queueOffset, + Long timestamp, String queueName, - Schema schema, String shardingKey, - Object[] payload) { - super(timestamp, entryType, queueName, schema, shardingKey, payload); - this.queueOffset = queueOffset; + EntryType entryType, + MetaAndData key, + MetaAndData value) { + return new SinkDataEntry(queueOffset, timestamp, queueName, shardingKey, entryType, key, value, + getHeaders().duplicate()); } - /** - * Offset in the message queue. - */ - private Long queueOffset; + public SinkDataEntry newRecord(Long queueOffset, + Long timestamp, + String queueName, + String shardingKey, + EntryType entryType, + MetaAndData key, + MetaAndData value, + Headers headers) { + return new SinkDataEntry(queueOffset, timestamp, queueName, shardingKey, entryType, key, value, headers); + } - public Long getQueueOffset() { - return queueOffset; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + SinkDataEntry that = (SinkDataEntry)o; + + return queueOffset.equals(that.queueOffset); } - public void setQueueOffset(Long queueOffset) { - this.queueOffset = queueOffset; + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Long.hashCode(queueOffset); + return result; } - @Override public String toString() { + @Override + public String toString() { return "SinkDataEntry{" + "queueOffset=" + queueOffset + "} " + super.toString(); } - @Override public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof SinkDataEntry)) - return false; - if (!super.equals(o)) - return false; - SinkDataEntry entry = (SinkDataEntry) o; - return Objects.equals(queueOffset, entry.queueOffset); + public Long getQueueOffset() { + return queueOffset; } - @Override public int hashCode() { - return Objects.hash(super.hashCode(), queueOffset); + public void setQueueOffset(Long queueOffset) { + this.queueOffset = queueOffset; } } diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/SourceDataEntry.java b/connector/src/main/java/io/openmessaging/connector/api/data/SourceDataEntry.java index bf07bbc..30a4026 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/data/SourceDataEntry.java +++ b/connector/src/main/java/io/openmessaging/connector/api/data/SourceDataEntry.java @@ -20,6 +20,8 @@ import java.nio.ByteBuffer; import java.util.Objects; +import io.openmessaging.connector.api.header.Headers; + /** * SourceDataEntries are generated by SourceTasks and passed to specific message queue to store. * @@ -27,39 +29,121 @@ * @since OMS 0.1.0 */ public class SourceDataEntry extends DataEntry { + /** + * Partition of the data source. + */ + private ByteBuffer sourcePartition; + + /** + * Position of current data entry of {@link SourceDataEntry#sourcePartition}. + */ + private ByteBuffer sourcePosition; public SourceDataEntry(ByteBuffer sourcePartition, ByteBuffer sourcePosition, Long timestamp, + String queueName, + String shardingKey, EntryType entryType, + MetaAndData key, + MetaAndData value, + Headers headers) { + super(timestamp, queueName, shardingKey, entryType, key, value, headers); + this.sourcePartition = sourcePartition; + this.sourcePosition = sourcePosition; + } + + public SourceDataEntry(ByteBuffer sourcePartition, + ByteBuffer sourcePosition, String queueName, - Schema schema, - Object[] payload) { - this(sourcePartition, sourcePosition, timestamp, entryType, queueName, schema, null, payload); + String shardingKey, + EntryType entryType, + MetaAndData value, + Headers headers) { + this(sourcePartition, sourcePosition, null, + queueName, shardingKey, entryType, + null, value, headers); } public SourceDataEntry(ByteBuffer sourcePartition, ByteBuffer sourcePosition, - Long timestamp, + String queueName, + String shardingKey, + EntryType entryType, + MetaAndData value) { + this(sourcePartition, sourcePosition, null, + queueName, shardingKey, entryType, + null, value, null); + } + + public SourceDataEntry(ByteBuffer sourcePartition, + ByteBuffer sourcePosition, + String queueName, + EntryType entryType, + MetaAndData value) { + this(sourcePartition, sourcePosition, null, + queueName, null, entryType, + null, value, null); + } + + public SourceDataEntry(ByteBuffer sourcePartition, + ByteBuffer sourcePosition, + String queueName, + EntryType entryType, + MetaAndData key, + MetaAndData value) { + this(sourcePartition, sourcePosition, null, + queueName, null, entryType, + key, value, null); + } + + public SourceDataEntry(ByteBuffer sourcePartition, + ByteBuffer sourcePosition, + String queueName, + String shardingKey, EntryType entryType, + MetaAndData key, + MetaAndData value) { + this(sourcePartition, sourcePosition, null, + queueName, shardingKey, entryType, + key, value, null); + } + + public SourceDataEntry(ByteBuffer sourcePartition, + ByteBuffer sourcePosition, + Long timestamp, String queueName, - Schema schema, String shardingKey, - Object[] payload) { - super(timestamp, entryType, queueName, schema, shardingKey, payload); - this.sourcePartition = sourcePartition; - this.sourcePosition = sourcePosition; + EntryType entryType, + MetaAndData key, + MetaAndData value) { + this(sourcePartition, sourcePosition, timestamp, + queueName, shardingKey, entryType, + key, value, null); } - /** - * Partition of the data source. - */ - private ByteBuffer sourcePartition; + public SourceDataEntry newDataEntry( + Long timestamp, + String queueName, + String shardingKey, + EntryType entryType, + MetaAndData key, + MetaAndData value) { + return new SourceDataEntry(this.sourcePartition, this.sourcePosition, + timestamp, queueName, shardingKey, entryType, key, value, getHeaders().duplicate()); + } - /** - * Position of current data entry of {@link SourceDataEntry#sourcePartition}. - */ - private ByteBuffer sourcePosition; + public SourceDataEntry newDataEntry( + Long timestamp, + String queueName, + String shardingKey, + EntryType entryType, + MetaAndData key, + MetaAndData value, + Headers headers) { + return new SourceDataEntry(this.sourcePartition, this.sourcePosition, + timestamp, queueName, shardingKey, entryType, key, value, headers); + } public ByteBuffer getSourcePartition() { return sourcePartition; @@ -77,26 +161,37 @@ public void setSourcePosition(ByteBuffer sourcePosition) { this.sourcePosition = sourcePosition; } - @Override public String toString() { - return "SourceDataEntry{" + - "sourcePartition=" + sourcePartition + - ", sourcePosition=" + sourcePosition + - "} " + super.toString(); - } - - @Override public boolean equals(Object o) { - if (this == o) + @Override + public boolean equals(Object o) { + if (this == o) { return true; - if (!(o instanceof SourceDataEntry)) + } + if (o == null || getClass() != o.getClass()) { return false; - if (!super.equals(o)) + } + if (!super.equals(o)) { return false; - SourceDataEntry entry = (SourceDataEntry) o; - return Objects.equals(sourcePartition, entry.sourcePartition) && - Objects.equals(sourcePosition, entry.sourcePosition); + } + + SourceDataEntry that = (SourceDataEntry)o; + + return Objects.equals(this.sourcePartition, that.sourcePartition) && + Objects.equals(this.sourcePosition, that.sourcePosition); } - @Override public int hashCode() { - return Objects.hash(super.hashCode(), sourcePartition, sourcePosition); + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (this.sourcePartition != null ? this.sourcePartition.hashCode() : 0); + result = 31 * result + (this.sourcePosition != null ? this.sourcePosition.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SourceDataEntry{" + + "sourcePartition=" + this.sourcePartition + + ", sourcePosition=" + this.sourcePosition + + "} " + super.toString(); } } diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Struct.java b/connector/src/main/java/io/openmessaging/connector/api/data/Struct.java new file mode 100644 index 0000000..6afb3c0 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Struct.java @@ -0,0 +1,247 @@ +package io.openmessaging.connector.api.data; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * A structured record containing a set of named fields with values, each field using an independent {@link Meta}. + * Struct objects must specify a complete {@link Meta} up front, and only fields specified in the Meta may be set. + * + * @author liuboyan + */ +public class Struct { + + private final Meta meta; + private final Object[] values; + + public Struct(Meta meta) { + if (meta.getType() != Type.STRUCT) { + throw new RuntimeException("meta type should be STRUCT."); + } + this.meta = meta; + this.values = new Object[meta.getFields().size()]; + } + + public Meta meta() { + return meta; + } + + public Object get(String fieldName) { + Field field = lookupField(fieldName); + return get(field); + } + + public Object get(Field field) { + return values[field.index()]; + } + + public List getFields() { + return this.meta.getFields(); + } + + // Note that all getters have to have boxed return types since the fields might be optional + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Byte. + */ + public Byte getInt8(String fieldName) { + return (Byte)getCheckType(fieldName, Type.INT8); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Short. + */ + public Short getInt16(String fieldName) { + return (Short)getCheckType(fieldName, Type.INT16); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Integer. + */ + public Integer getInt32(String fieldName) { + return (Integer)getCheckType(fieldName, Type.INT32); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Long. + */ + public Long getInt64(String fieldName) { + return (Long)getCheckType(fieldName, Type.INT64); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Float. + */ + public Float getFloat32(String fieldName) { + return (Float)getCheckType(fieldName, Type.FLOAT32); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Double. + */ + public Double getFloat64(String fieldName) { + return (Double)getCheckType(fieldName, Type.FLOAT64); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Boolean. + */ + public Boolean getBoolean(String fieldName) { + return (Boolean)getCheckType(fieldName, Type.BOOLEAN); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a String. + */ + public String getString(String fieldName) { + return (String)getCheckType(fieldName, Type.STRING); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a byte[]. + */ + public byte[] getBytes(String fieldName) { + Object bytes = getCheckType(fieldName, Type.BYTES); + if (bytes instanceof ByteBuffer) { + return ((ByteBuffer)bytes).array(); + } + return (byte[])bytes; + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a List. + */ + @SuppressWarnings("unchecked") + public List getArray(String fieldName) { + return (List)getCheckType(fieldName, Type.ARRAY); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Map. + */ + @SuppressWarnings("unchecked") + public Map getMap(String fieldName) { + return (Map)getCheckType(fieldName, Type.MAP); + } + + /** + * Equivalent to calling {@link #get(String)} and casting the result to a Struct. + */ + public Struct getStruct(String fieldName) { + return (Struct)getCheckType(fieldName, Type.STRUCT); + } + + public Object getObject(String fieldName) { + Field field = lookupField(fieldName); + return values[field.index()]; + } + + /** + * Set the value of a field. Validates the value, throwing a {@link RuntimeException} if it does not match the + * field's + * {@link Meta}. + * + * @param fieldName the name of the field to set + * @param value the value of the field + * @return the Struct, to allow chaining of {@link #put(String, Object)} calls + */ + public Struct put(String fieldName, Object value) { + Field field = lookupField(fieldName); + return put(field, value); + } + + /** + * Set the value of a field. Validates the value, throwing a {@link RuntimeException} if it does not match the + * field's + * {@link Meta}. + * + * @param field the field to set + * @param value the value of the field + * @return the Struct, to allow chaining of {@link #put(String, Object)} calls + */ + public Struct put(Field field, Object value) { + if (null == field) { + throw new RuntimeException("field cannot be null."); + } + Meta.validateValue(field.name(), field.meta(), value); + values[field.index()] = value; + return this; + } + + public MetaAndData toMetaData() { + return new MetaAndData(this.meta, this); + } + + /** + * Validates that this struct has filled in all the necessary data with valid values. For required fields + * without defaults, this validates that a value has been set and has matching types/metas. If any validation + * fails, throws a DataException. + */ + public void validate() { + for (Field field : meta.getFields()) { + Meta fieldMeta = field.meta(); + Object value = values[field.index()]; + Meta.validateValue(field.name(), fieldMeta, value); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Struct struct = (Struct)o; + return Objects.equals(meta, struct.meta) && + Arrays.deepEquals(values, struct.values); + } + + @Override + public int hashCode() { + return Objects.hash(meta, Arrays.deepHashCode(values)); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Struct{["); + boolean first = true; + for (int i = 0; i < values.length; i++) { + final Object value = values[i]; + if (value != null) { + final Field field = meta.getFields().get(i); + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append(field.toString()).append("=").append(value); + } + } + return sb.append("]}").toString(); + } + + private Field lookupField(String fieldName) { + Field field = meta.getFieldByName(fieldName); + if (field == null) { + throw new RuntimeException(fieldName + " is not a valid field name"); + } + return field; + } + + // Get the field's value, but also check that the field matches the specified type, throwing an exception if it + // doesn't. + // Used to implement the get*() methods that return typed data instead of Object + private Object getCheckType(String fieldName, Type type) { + Field field = lookupField(fieldName); + if (field.meta().getType() != type) { + throw new RuntimeException("Field '" + fieldName + "' is not of type " + type); + } + return values[field.index()]; + } + +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Time.java b/connector/src/main/java/io/openmessaging/connector/api/data/Time.java new file mode 100644 index 0000000..23541a1 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Time.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.data; + + +import java.util.Calendar; +import java.util.TimeZone; + +/** + *

+ * A time representing a specific point in a day, not tied to any specific date. The corresponding Java type is a + * java.util.Date where only hours, minutes, seconds, and milliseconds can be non-zero. This effectively makes it a + * point in time during the first day after the Unix epoch. The underlying representation is an integer + * representing the number of milliseconds after midnight. + *

+ */ +public class Time { + public static final String LOGICAL_NAME = "io.openmessaging.connector.api.data.Time"; + + private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + /** + * Returns a MetaBuilder for a Time. By returning a MetaBuilder you can override additional meta settings such + * as required/optional, default value, and documentation. + * @return a MetaBuilder + */ + public static MetaBuilder builder() { + return MetaBuilder.int32() + .name(LOGICAL_NAME); + } + + public static final Meta META = builder().meta(); + + /** + * Convert a value from its logical format (Time) to it's encoded format. + * @param value the logical value + * @return the encoded value + */ + public static int fromLogical(Meta meta, java.util.Date value) { + if (!(LOGICAL_NAME.equals(meta.getName()))) { + throw new RuntimeException("Requested conversion of Time object but the meta does not match."); + } + Calendar calendar = Calendar.getInstance(UTC); + calendar.setTime(value); + long unixMillis = calendar.getTimeInMillis(); + if (unixMillis < 0 || unixMillis > MILLIS_PER_DAY) { + throw new RuntimeException("RocketMQ Connect Time type should not have any date fields set to non-zero values."); + } + return (int) unixMillis; + } + + public static java.util.Date toLogical(Meta meta, int value) { + if (!(LOGICAL_NAME.equals(meta.getName()))) { + throw new RuntimeException("Requested conversion of Date object but the meta does not match."); + } + if (value < 0 || value > MILLIS_PER_DAY) { + throw new RuntimeException("Time values must use number of milliseconds greater than 0 and less than 86400000"); + } + return new java.util.Date(value); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Timestamp.java b/connector/src/main/java/io/openmessaging/connector/api/data/Timestamp.java new file mode 100644 index 0000000..71ac0eb --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Timestamp.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.data; + + +/** + *

+ * A timestamp representing an absolute time, without timezone information. The corresponding Java type is a + * java.util.Date. The underlying representation is a long representing the number of milliseconds since Unix epoch. + *

+ */ +public class Timestamp { + public static final String LOGICAL_NAME = "io.openmessaging.connector.api.data.Timestamp"; + + /** + * Returns a MetaBuilder for a Timestamp. By returning a MetaBuilder you can override additional meta settings such + * as required/optional, default value, and documentation. + * @return a MetaBuilder + */ + public static MetaBuilder builder() { + return MetaBuilder.int64() + .name(LOGICAL_NAME); + } + + public static final Meta META = builder().meta(); + + /** + * Convert a value from its logical format (Date) to it's encoded format. + * @param value the logical value + * @return the encoded value + */ + public static long fromLogical(Meta meta, java.util.Date value) { + if (!(LOGICAL_NAME.equals(meta.getName()))) { + throw new RuntimeException("Requested conversion of Timestamp object but the meta does not match."); + } + return value.getTime(); + } + + public static java.util.Date toLogical(Meta meta, long value) { + if (!(LOGICAL_NAME.equals(meta.getName()))) { + throw new RuntimeException("Requested conversion of Timestamp object but the meta does not match."); + } + return new java.util.Date(value); + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/data/Type.java b/connector/src/main/java/io/openmessaging/connector/api/data/Type.java new file mode 100644 index 0000000..b36de47 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/data/Type.java @@ -0,0 +1,96 @@ +package io.openmessaging.connector.api.data; + +import java.util.Locale; + +/** + * The type of a meta. These only include the core types; + * logical types must be determined by checking the meta name. + */ +public enum Type { + /** + * 8-bit signed integer + */ + INT8, + /** + * 16-bit signed integer + */ + INT16, + /** + * 32-bit signed integer + */ + INT32, + /** + * 64-bit signed integer + */ + INT64, + /** + * BigInteger + */ + BIG_INTEGER, + /** + * 32-bit IEEE 754 floating point number + */ + FLOAT32, + /** + * 64-bit IEEE 754 floating point number + */ + FLOAT64, + /** + * Boolean value (true or false) + */ + BOOLEAN, + /** + * Character string that supports all Unicode characters. + * + * Note that this does not imply any specific encoding (e.g. UTF-8) as this is an in-memory representation. + */ + STRING, + /** + * Sequence of unsigned 8-bit bytes + */ + BYTES, + /** + * Date + */ + DATETIME, + + /** + * An ordered sequence of elements, each of which shares the same type. + */ + ARRAY, + /** + * A mapping from keys to values. Both keys and values can be arbitrarily complex types, including complex types + */ + MAP, + /** + * A structured record containing a set of named fields, each field using a fixed, independent. + */ + STRUCT; + + private String name; + + Type() { + this.name = this.name().toLowerCase(Locale.ROOT); + } + + public String getName() { + return name; + } + + public boolean isPrimitive() { + switch (this) { + case INT8: + case INT16: + case INT32: + case INT64: + case FLOAT32: + case FLOAT64: + case BOOLEAN: + case STRING: + case BYTES: + return true; + } + return false; + } + +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/header/DataHeader.java b/connector/src/main/java/io/openmessaging/connector/api/header/DataHeader.java new file mode 100644 index 0000000..20b935a --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/header/DataHeader.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.header; + + +import java.util.Objects; + +import io.openmessaging.connector.api.data.Meta; +import io.openmessaging.connector.api.data.MetaAndData; + +/** + * A {@link Header} implementation. + */ +public class DataHeader implements Header { + + private final String key; + private final MetaAndData metaAndData; + + public DataHeader(String key, MetaAndData metaAndData) { + Objects.requireNonNull(key, "Null header keys are not permitted"); + this.key = key; + this.metaAndData = metaAndData; + } + + public DataHeader(String key, Meta meta, Object value) { + this(key,new MetaAndData(meta, value)); + } + + @Override + public MetaAndData data() { + return this.metaAndData; + } + + @Override + public String key() { + return this.key; + } + + @Override + public Header rename(String key) { + Objects.requireNonNull(key, "Null header keys are not permitted"); + if (this.key.equals(key)) { + return this; + } + return new DataHeader(key, this.metaAndData); + } + + @Override + public Header with(Meta meta, Object value) { + return new DataHeader(key, this.metaAndData); + } + + @Override + public int hashCode() { + return Objects.hash(this.key, this.metaAndData); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Header) { + Header that = (Header) obj; + return Objects.equals(this.key, that.key()) + && Objects.equals(this.data(), that.data()); + } + return false; + } + + @Override + public String toString() { + return "DataHeader(key=" + key + ", value=" + data().getData() + ", meta=" + data().getMeta() + ")"; + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/header/DataHeaders.java b/connector/src/main/java/io/openmessaging/connector/api/header/DataHeaders.java new file mode 100644 index 0000000..b5bd2b7 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/header/DataHeaders.java @@ -0,0 +1,502 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.header; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; + +import io.openmessaging.connector.api.data.Date; +import io.openmessaging.connector.api.data.Decimal; +import io.openmessaging.connector.api.data.Meta; +import io.openmessaging.connector.api.data.Struct; +import io.openmessaging.connector.api.data.Time; +import io.openmessaging.connector.api.data.Timestamp; +import io.openmessaging.connector.api.data.Type; + +/** + * A basic {@link Headers} implementation. + */ +public class DataHeaders implements Headers { + + private static final int EMPTY_HASH = Objects.hash(new LinkedList<>()); + + /** + * An immutable and therefore sharable empty iterator. + */ + private static final Iterator
EMPTY_ITERATOR = new Iterator
() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public Header next() { + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new IllegalStateException(); + } + }; + + /** + * This field is set lazily, but once set to a list it is never set back to null + */ + private Map headers; + + public DataHeaders() { + } + + public DataHeaders(Iterable
original) { + if (original == null) { + return; + } + if (original instanceof DataHeaders) { + DataHeaders originalHeaders = (DataHeaders) original; + if (!originalHeaders.isEmpty()) { + headers = new LinkedHashMap<>(originalHeaders.headers); + } + } else { + headers = new LinkedHashMap<>(); + for (Header header : original) { + headers.put(header.key(), header); + } + } + } + + @Override + public int size() { + return headers == null ? 0 : headers.size(); + } + + @Override + public boolean isEmpty() { + return headers == null ? true : headers.isEmpty(); + } + + @Override + public Headers clear() { + if (headers != null) { + headers.clear(); + } + return this; + } + + @Override + public Headers add(Header header) { + Objects.requireNonNull(header, "Unable to add a null header."); + if (headers == null) { + headers = new LinkedHashMap<>(); + } + headers.put(header.key(), header); + return this; + } + + protected Headers addWithoutValidating(String key, Object value, Meta meta) { + return add(new DataHeader(key, meta, value)); + } + + @Override + public Headers add(String key, Meta meta, Object value) { + checkMetaMatches(meta, value); + return add(new DataHeader(key, meta, value)); + } + + @Override + public Headers addString(String key, String value) { + return addWithoutValidating(key, value, Meta.STRING_META); + } + + @Override + public Headers addBytes(String key, byte[] value) { + return addWithoutValidating(key, value, Meta.BYTES_META); + } + + @Override + public Headers addBoolean(String key, boolean value) { + return addWithoutValidating(key, value, Meta.BOOLEAN_META); + } + + @Override + public Headers addByte(String key, byte value) { + return addWithoutValidating(key, value, Meta.INT8_META); + } + + @Override + public Headers addShort(String key, short value) { + return addWithoutValidating(key, value, Meta.INT16_META); + } + + @Override + public Headers addInt(String key, int value) { + return addWithoutValidating(key, value, Meta.INT32_META); + } + + @Override + public Headers addLong(String key, long value) { + return addWithoutValidating(key, value, Meta.INT64_META); + } + + @Override + public Headers addFloat(String key, float value) { + return addWithoutValidating(key, value, Meta.FLOAT32_META); + } + + @Override + public Headers addDouble(String key, double value) { + return addWithoutValidating(key, value, Meta.FLOAT64_META); + } + + @Override + public Headers addList(String key, List value, Meta meta) { + if (value == null) { + return add(key, null, null); + } + checkMetaType(meta, Type.ARRAY); + return addWithoutValidating(key, value, meta); + } + + @Override + public Headers addMap(String key, Map value, Meta meta) { + if (value == null) { + return add(key, null, null); + } + checkMetaType(meta, Type.MAP); + return addWithoutValidating(key, value, meta); + } + + @Override + public Headers addStruct(String key, Struct value) { + if (value == null) { + return add(key, null, null); + } + checkMetaType(value.meta(), Type.STRUCT); + return addWithoutValidating(key, value, value.meta()); + } + + @Override + public Headers addDecimal(String key, BigDecimal value) { + if (value == null) { + return add(key, null, null); + } + // Check that this is a decimal ... + Meta meta = Decimal.meta(value.scale()); + Decimal.fromLogical(meta, value); + return addWithoutValidating(key, value, meta); + } + + @Override + public Headers addDate(String key, java.util.Date value) { + if (value != null) { + // Check that this is a date ... + Date.fromLogical(Date.META, value); + } + return addWithoutValidating(key, value, Date.META); + } + + @Override + public Headers addTime(String key, java.util.Date value) { + if (value != null) { + // Check that this is a time ... + Time.fromLogical(Time.META, value); + } + return addWithoutValidating(key, value, Time.META); + } + + @Override + public Headers addTimestamp(String key, java.util.Date value) { + if (value != null) { + // Check that this is a timestamp ... + Timestamp.fromLogical(Timestamp.META, value); + } + return addWithoutValidating(key, value, Timestamp.META); + } + + @Override + public Header findHeader(String key) { + return new FilterByKeyIterator(iterator(), key).makeNext(); + } + + @Override + public Map toMap(){ + return new HashMap<>(this.headers); + } + + @Override + public Iterator
iterator() { + if (headers != null) { + return headers.values().iterator(); + } + return EMPTY_ITERATOR; + } + + @Override + public Headers remove(String key) { + checkKey(key); + if (!isEmpty()) { + Iterator
iterator = iterator(); + while (iterator.hasNext()) { + if (iterator.next().key().equals(key)) { + iterator.remove(); + } + } + } + return this; + } + @Override + public int hashCode() { + return isEmpty() ? EMPTY_HASH : Objects.hash(headers); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Headers) { + Headers that = (Headers) obj; + Iterator
thisIter = this.iterator(); + Iterator
thatIter = that.iterator(); + while (thisIter.hasNext() && thatIter.hasNext()) { + if (!Objects.equals(thisIter.next(), thatIter.next())) { + return false; + } + } + return !thisIter.hasNext() && !thatIter.hasNext(); + } + return false; + } + + @Override + public String toString() { + return "DataHeaders(headers=" + (headers != null ? headers : "") + ")"; + } + + @Override + public DataHeaders duplicate() { + return new DataHeaders(this); + } + + /** + * Check that the key is not null + * + * @param key the key; may not be null + * @throws NullPointerException if the supplied key is null + */ + private void checkKey(String key) { + Objects.requireNonNull(key, "Header key cannot be null"); + } + + /** + * Check the {@link Type() meta's type} matches the specified type. + * + * @param meta the meta; never null + * @param type the expected type + * @throws RuntimeException if the meta's type does not match the expected type + */ + private void checkMetaType(Meta meta, Type type) { + if (meta.getType() != type) { + throw new RuntimeException("Expecting " + type + " but instead found " + meta.getType()); + } + } + + /** + * Check that the value and its meta are compatible. + * + * @param meta + * @param value the meta and value pair + * @throws RuntimeException if the meta is not compatible with the value + */ + private void checkMetaMatches(Meta meta, Object value) { + if (meta != null && value != null) { + switch (meta.getType()) { + case BYTES: + if (value instanceof ByteBuffer) { + return; + } + if (value instanceof byte[]) { + return; + } + if (value instanceof BigDecimal && Decimal.LOGICAL_NAME.equals(meta.getName())) { + return; + } + break; + case STRING: + if (value instanceof String) { + return; + } + break; + case BOOLEAN: + if (value instanceof Boolean) { + return; + } + break; + case INT8: + if (value instanceof Byte) { + return; + } + break; + case INT16: + if (value instanceof Short) { + return; + } + break; + case INT32: + if (value instanceof Integer) { + return; + } + if (value instanceof java.util.Date && Date.LOGICAL_NAME.equals(meta.getName())) { + return; + } + if (value instanceof java.util.Date && Time.LOGICAL_NAME.equals(meta.getName())) { + return; + } + break; + case INT64: + if (value instanceof Long) { + return; + } + if (value instanceof java.util.Date && Timestamp.LOGICAL_NAME.equals(meta.getName())) { + return; + } + break; + case FLOAT32: + if (value instanceof Float) { + return; + } + break; + case FLOAT64: + if (value instanceof Double) { + return; + } + break; + case ARRAY: + if (value instanceof List) { + return; + } + break; + case MAP: + if (value instanceof Map) { + return; + } + break; + case STRUCT: + if (value instanceof Struct) { + return; + } + break; + } + throw new RuntimeException("The value " + value + " is not compatible with the meta " + meta); + } + } + + private static final class FilterByKeyIterator implements Iterator
{ + private enum State { + READY, NOT_READY, DONE, FAILED + } + + private State state = State.NOT_READY; + private Header next; + + + private final Iterator
original; + private final String key; + + private FilterByKeyIterator(Iterator
original, String key) { + this.original = original; + this.key = key; + } + + protected Header makeNext() { + while (original.hasNext()) { + Header header = original.next(); + if (!header.key().equals(key)) { + continue; + } + return header; + } + return this.allDone(); + } + + @Override + public boolean hasNext() { + switch (state) { + case FAILED: + throw new IllegalStateException("Iterator is in failed state"); + case DONE: + return false; + case READY: + return true; + default: + return maybeComputeNext(); + } + } + + @Override + public Header next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + state = State.NOT_READY; + if (next == null) { + throw new IllegalStateException("Expected item but none found."); + } + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Removal not supported"); + } + + + + + public Header peek() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return next; + } + + protected Header allDone() { + state = State.DONE; + return null; + } + + + private Boolean maybeComputeNext() { + state = State.FAILED; + next = makeNext(); + if (state == State.DONE) { + return false; + } else { + state = State.READY; + return true; + } + } + } +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/header/Header.java b/connector/src/main/java/io/openmessaging/connector/api/header/Header.java new file mode 100644 index 0000000..647d439 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/header/Header.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.header; + +import io.openmessaging.connector.api.data.Meta; +import io.openmessaging.connector.api.data.MetaAndData; + +/** + * A {@link Header} is a key-value pair, and multiple headers can be included with the key, value, and timestamp in each RocketMQ message. + * The data contains both the meta information and the value object. + *

+ * This is an immutable interface. + */ +public interface Header { + + /** + * Get the header's value as deserialized by Connect's header converter. + * + * @return + */ + MetaAndData data(); + + /** + * The header's key, which is not necessarily unique within the set of headers on a RocketMQ message. + * + * @return the header's key; never null + */ + String key(); + + /** + * Return a new {@link Header} object that has the same key but with the supplied value. + * + * @param meta the meta for the new value; may be null + * @param value the new value + * @return the new {@link Header}; never null + */ + Header with(Meta meta, Object value); + + /** + * Return a new {@link Header} object that has the same meta and value but with the supplied key. + * + * @param key the key for the new header; may not be null + * @return the new {@link Header}; never null + */ + Header rename(String key); +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/header/Headers.java b/connector/src/main/java/io/openmessaging/connector/api/header/Headers.java new file mode 100644 index 0000000..de72637 --- /dev/null +++ b/connector/src/main/java/io/openmessaging/connector/api/header/Headers.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.connector.api.header; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import io.openmessaging.connector.api.data.Meta; +import io.openmessaging.connector.api.data.Struct; + +/** + * A mutable ordered collection of {@link Header} objects. + * Note that multiple headers shouldn't have the same {@link Header#key() key}. + */ +public interface Headers extends Iterable

{ + + /** + * Get the number of headers in this object. + * + * @return the number of headers; never negative + */ + int size(); + + /** + * Determine whether this object has no headers. + * + * @return true if there are no headers, or false if there is at least one header + */ + boolean isEmpty(); + + /** + * Get the collection of {@link Header} objects whose {@link Header#key() keys} all match the specified key. + * + * @param key the key; may not be null + * @return the iterator over headers with the specified key; may be null if there are no headers with the + * specified key + */ + Header findHeader(String key); + + /** + * Get the map of {@link Header} objects. + * + * @return the map of headers + */ + Map toMap(); + + /** + * Add the given {@link Header} to this collection. + * + * @param header the header; may not be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers add(Header header); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @param meta the meta for the header's value; may not be null if the value is not null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers add(String key, Meta meta, Object value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addString(String key, String value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addBoolean(String key, boolean value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addByte(String key, byte value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addShort(String key, short value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addInt(String key, int value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addLong(String key, long value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addFloat(String key, float value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addDouble(String key, double value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addBytes(String key, byte[] value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @param meta the meta describing the list value; may not be null + * @return this object to facilitate chaining multiple methods; never null + * @throws Exception if the header's value is invalid + */ + Headers addList(String key, List value, Meta meta); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @param meta the meta describing the map value; may not be null + * @return this object to facilitate chaining multiple methods; never null + * @throws Exception if the header's value is invalid + */ + Headers addMap(String key, Map value, Meta meta); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + * @throws Exception if the header's value is invalid + */ + Headers addStruct(String key, Struct value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addDecimal(String key, BigDecimal value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addDate(String key, java.util.Date value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addTime(String key, java.util.Date value); + + /** + * Add to this collection a {@link Header} with the given key and value. + * + * @param key the header's key; may not be null + * @param value the header's value; may be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers addTimestamp(String key, java.util.Date value); + + /** + * Removes all {@link Header} objects whose {@link Header#key() key} matches the specified key. + * + * @param key the key; may not be null + * @return this object to facilitate chaining multiple methods; never null + */ + Headers remove(String key); + + /** + * Removes all headers from this object. + * + * @return this object to facilitate chaining multiple methods; never null + */ + Headers clear(); + + /** + * Create a copy of this {@link Headers} object. The new copy will contain all of the same {@link Header} objects as + * this object. + * + * @return the copy; never null + */ + Headers duplicate(); + +} diff --git a/connector/src/main/java/io/openmessaging/connector/api/sink/SinkTask.java b/connector/src/main/java/io/openmessaging/connector/api/sink/SinkTask.java index c2e619c..f718e3a 100644 --- a/connector/src/main/java/io/openmessaging/connector/api/sink/SinkTask.java +++ b/connector/src/main/java/io/openmessaging/connector/api/sink/SinkTask.java @@ -20,6 +20,7 @@ import io.openmessaging.connector.api.Task; import io.openmessaging.connector.api.common.QueueMetaData; import io.openmessaging.connector.api.data.SinkDataEntry; + import java.util.Collection; import java.util.Map; diff --git a/connector/src/main/test/io/openmessaging/connector/api/data/HeadersTest.java b/connector/src/main/test/io/openmessaging/connector/api/data/HeadersTest.java new file mode 100644 index 0000000..3ac4c4a --- /dev/null +++ b/connector/src/main/test/io/openmessaging/connector/api/data/HeadersTest.java @@ -0,0 +1,49 @@ +package io.openmessaging.connector.api.data; + +import java.math.BigDecimal; +import java.util.Map; + +import io.openmessaging.connector.api.header.DataHeader; +import io.openmessaging.connector.api.header.DataHeaders; +import io.openmessaging.connector.api.header.Header; +import io.openmessaging.connector.api.header.Headers; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class HeadersTest { + + + @Test + public void commonTest(){ + Meta strMeta = MetaBuilder.string().name("myStrMeta").build(); + Header header = new DataHeader("header1", strMeta, "headerValue1"); + //System.out.println("construct header: " + header); + assertNotNull(header); + + Headers headers = new DataHeaders(); + headers.add(header) + .add("header1", strMeta, "headerValue2") + .addBoolean("header2", true) + .addDecimal("header3 ", BigDecimal.ONE) + ; + //System.out.println("construct headers: " + headers); + assertNotNull(headers); + + Header findHeader1 = headers.findHeader("header1"); + //System.out.println("except header named header1, real is: " + findHeader1); + assertEquals(findHeader1.key(), "header1"); + findHeader1.rename("header2"); + + Header findHeader2 = headers.findHeader("header2"); + //System.out.println("except header valued TRUE, real is: " + findHeader2); + assertEquals(findHeader2.key(), "header2"); + + Map headerMap = headers.toMap(); + //System.out.println("show header Map: " + headerMap); + assertNotNull(headerMap); + headerMap.put("header3", header); + } + +} diff --git a/connector/src/main/test/io/openmessaging/connector/api/data/MetaAndDataTest.java b/connector/src/main/test/io/openmessaging/connector/api/data/MetaAndDataTest.java new file mode 100644 index 0000000..1de0448 --- /dev/null +++ b/connector/src/main/test/io/openmessaging/connector/api/data/MetaAndDataTest.java @@ -0,0 +1,105 @@ +package io.openmessaging.connector.api.data; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class MetaAndDataTest { + + + @Test + public void structParserTest(){ + Meta structMeta = MetaBuilder.struct() + .name("structMeta") + .field("f1", Meta.STRING_META) + .field("f2", Meta.INT32_META) + .field("f3", MetaBuilder.array(Meta.STRING_META).build()) + .build(); + MetaAndData structMetaAndData = new MetaAndData( + structMeta + ); + + structMetaAndData.putData("f1", "asdf"); + structMetaAndData.putData("f2", 32); + structMetaAndData.putData("f3", new ArrayList(){{ + add("a"); + add("b"); + }}); + + String jsonStr = structMetaAndData.convertToString(); + assertNotNull(jsonStr); + //System.out.println(jsonStr); + + // MAP MetaAndData + MetaAndData newMetaAndData = MetaAndData.getMetaDataFromString(jsonStr); + assertNotNull(newMetaAndData); + //System.out.println(newMetaAndData); + + // MAP TO STRUCT + Struct struct = newMetaAndData.convertToStruct(); + assertNotNull(struct); + //System.out.println(struct); + } + + @Test + public void listParserTest(){ + String str = "[1, 2, 3, \"four\"]"; + MetaAndData result = MetaAndData.getMetaDataFromString(str); + + List list = (List) result.getData(); + //System.out.println(list); + assertEquals(4, list.size()); + assertEquals(1, ((Number) list.get(0)).intValue()); + assertEquals(2, ((Number) list.get(1)).intValue()); + assertEquals(3, ((Number) list.get(2)).intValue()); + assertEquals("four", list.get(3)); + } + + @Test + public void commonTest(){ + MetaAndData metaAndData = new MetaAndData(Decimal.builder(10).build(), BigDecimal.ONE); + assertNotNull(metaAndData); + + BigDecimal de = metaAndData.convertToDecimal(10); + assertEquals(de, BigDecimal.ONE); + assertNotEquals(de, 1); + + BigDecimal de2 = metaAndData.convertToDecimal(1); + assertEquals(de2, BigDecimal.ONE); + + metaAndData.putData(BigDecimal.TEN); + assertEquals(metaAndData.convertToDecimal(10), BigDecimal.TEN); + + assertEquals((BigDecimal)metaAndData.getData(), BigDecimal.TEN); + + assertNull(metaAndData.inferMeta()); + } + + @Test + public void stringTest(){ + MetaAndData metaAndData = new MetaAndData(Meta.STRING_META, "message value 中文信息"); + assertNotNull(metaAndData); + + String msg = metaAndData.convertToString(); + assertEquals(msg, "message value 中文信息"); + + Exception ex = null; + try{ + metaAndData.convertToDouble(); + }catch (Exception e){ + ex = e; + } + assertNotNull(ex); + } + + + + +} diff --git a/connector/src/main/test/io/openmessaging/connector/api/data/MetaTest.java b/connector/src/main/test/io/openmessaging/connector/api/data/MetaTest.java new file mode 100644 index 0000000..9420e63 --- /dev/null +++ b/connector/src/main/test/io/openmessaging/connector/api/data/MetaTest.java @@ -0,0 +1,508 @@ +package io.openmessaging.connector.api.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class MetaTest { + + @Test + public void staticMethod(){ + Type type = Meta.getMetaType(String.class); + assertEquals(type, Type.STRING); + type = Meta.getMetaType(Struct.class); + assertEquals(type, Type.STRUCT); + type = Meta.getMetaType(Map.class); + assertEquals(type, Type.MAP); + type = Meta.getMetaType(ArrayList.class); + assertEquals(type, Type.ARRAY); + + + + Meta.validateValue(Meta.BYTES_META, "kfc".getBytes()); + Exception error = null; + try{ + // Throw exception for the value of STRING do not match the meta of BYTES . + Meta.validateValue(Meta.BYTES_META, "kfc"); + }catch (Exception e){ + error = e; + } + assertNotNull(error); + + Meta.validateValue("name", Meta.STRING_META, "fasd"); + + List list = new ArrayList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + Object obj = list; + Meta.validateValue(MetaBuilder.array(Meta.STRING_META).build(), obj); + + error = null; + try{ + // Throw exception for the value of ARRAY do not match the meta of BYTES . + Meta.validateValue(MetaBuilder.array(Meta.BYTES_META).build(), obj); + }catch (Exception e){ + error = e; + } + assertNotNull(error); + } + + @Test + public void int8(){ + System.out.println("================INT8================"); + Meta meta1 = MetaBuilder.int8() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.int8() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.int8() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.int8() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.INT8, "abc", 1,"dataSource", null); + System.out.println(meta); + + System.out.println(Meta.INT8_META); + } + + @Test + public void int16(){ + System.out.println("================INT16================"); + Meta meta1 = MetaBuilder.int16() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.int16() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.int16() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.int16() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.INT16, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.INT16_META); + } + + + @Test + public void int32(){ + System.out.println("================INT32================"); + Meta meta1 = MetaBuilder.int32() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.int32() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.int32() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.int32() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.INT32, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.INT32_META); + } + + + + @Test + public void int64(){ + System.out.println("================INT64================"); + Meta meta1 = MetaBuilder.int64() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.int64() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.int64() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.int64() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.INT64, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.INT64_META); + } + + + + + @Test + public void float32(){ + System.out.println("================FLOAT32================"); + Meta meta1 = MetaBuilder.float32() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.float32() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.float32() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.float32() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.FLOAT32, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.FLOAT32_META); + } + + + @Test + public void float64(){ + System.out.println("================FLOAT64================"); + Meta meta1 = MetaBuilder.float64() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.float64() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.float64() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.float64() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.FLOAT64, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.FLOAT64_META); + } + + + @Test + public void bool(){ + System.out.println("================BOOLEAN================"); + Meta meta1 = MetaBuilder.bool() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.bool() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.bool() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.bool() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.BOOLEAN, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.BOOLEAN_META); + } + + + @Test + public void str(){ + System.out.println("================STRING================"); + Meta meta1 = MetaBuilder.string() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.string() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.string() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.string() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.STRING, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.STRING_META); + } + + + @Test + public void bytes(){ + System.out.println("================BYTES================"); + Meta meta1 = MetaBuilder.bytes() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.bytes() + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.bytes() + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.bytes() + .build(); + System.out.println(meta4); + + Meta meta = new MetaBase(Type.BYTES, "abc", 1, "dataSource", null); + System.out.println(meta); + + System.out.println(Meta.BYTES_META); + } + + + @Test + public void array(){ + System.out.println("================ARRAY================"); + Meta meta1 = MetaBuilder.array(Meta.STRING_META) + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.array(Meta.STRING_META) + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.array(Meta.STRING_META) + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.array(Meta.STRING_META) + .build(); + System.out.println(meta4); + + Meta meta = new MetaArray(Type.ARRAY, "abc", 1, "dataSource", + null, Meta.STRING_META); + System.out.println(meta); + + try{ + Meta metaError = new MetaMap(Type.MAP, "abc", 1, "dataSource", + null, Meta.STRING_META, Meta.BYTES_META); + }catch (Exception e){ + System.out.println("error for new MataMap but Type is not ARRAY."); + } + } + + + @Test + public void map(){ + System.out.println("================MAP================"); + Meta meta1 = MetaBuilder.map(Meta.STRING_META, Meta.BYTES_META) + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.map(Meta.STRING_META, Meta.BYTES_META) + .name("build2") + .dataSource("datasource1") + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.map(Meta.STRING_META, Meta.BYTES_META) + .dataSource("datasource1") + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.map(Meta.STRING_META, Meta.BYTES_META) + .build(); + System.out.println(meta4); + + Meta meta = new MetaMap(Type.MAP, "abc", 1, "dataSource", + null, Meta.STRING_META, Meta.BYTES_META); + System.out.println(meta); + + try{ + Meta metaError = new MetaMap(Type.ARRAY, "abc", 1, "dataSource", + null, Meta.STRING_META, Meta.BYTES_META); + }catch (Exception e){ + System.out.println("error for new MataMap but Type is not MAP."); + } + + } + + + @Test + public void struct(){ + System.out.println("================STRUCT================"); + Meta meta1 = MetaBuilder.struct() + .name("build1") + .version(100) + .dataSource("datasource1") + .parameter("p1","v1") + .parameter("p2", "v2") + .field("f1", Meta.STRING_META) + .field("f2", Meta.BOOLEAN_META) + .field("f3", Meta.FLOAT32_META) + .build(); + System.out.println(meta1); + + Meta meta2 = MetaBuilder.struct() + .name("build2") + .dataSource("datasource1") + .field("f1", Meta.STRING_META) + .field("f2", Meta.BOOLEAN_META) + .field("f3", Meta.FLOAT32_META) + .build(); + System.out.println(meta2); + + Meta meta3 = MetaBuilder.struct() + .dataSource("datasource1") + .field("f1", Meta.STRING_META) + .field("f2", Meta.BOOLEAN_META) + .field("f3", Meta.FLOAT32_META) + .build(); + System.out.println(meta3); + + Meta meta4 = MetaBuilder.struct() + .field("f1", Meta.STRING_META) + .field("f2", Meta.BOOLEAN_META) + .field("f3", Meta.FLOAT32_META) + .build(); + System.out.println(meta4); + + Meta meta5 = MetaBuilder.struct() + .build(); + System.out.println(meta5); + + int size = 10; + List fields = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Field field = new Field(i, "f"+i, Meta.STRING_META); + fields.add(field); + } + Meta meta = new MetaStruct(Type.STRUCT, "abc", 1, "dataSource", + null, fields); + System.out.println(meta); + + try{ + Meta metaError = new MetaStruct(Type.MAP, "abc", 1, "dataSource", + null, fields); + }catch (Exception e){ + System.out.println("error for new MetaStruct but Type is not STRUCT."); + } + + } + +} diff --git a/connector/src/main/test/io/openmessaging/connector/api/data/SinkDataEntryTest.java b/connector/src/main/test/io/openmessaging/connector/api/data/SinkDataEntryTest.java new file mode 100644 index 0000000..4fc128d --- /dev/null +++ b/connector/src/main/test/io/openmessaging/connector/api/data/SinkDataEntryTest.java @@ -0,0 +1,117 @@ +package io.openmessaging.connector.api.data; + + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class SinkDataEntryTest { + + /** + * simple test for kv data + * SET myset asldfjsaldjglas PX 360000 + */ + @Test + public void testKV() { + String command = "SET"; + String key = "myset"; + String value = "asldfjsaldjglas"; + long px = 360000; + + DataEntryBuilder builder = + DataEntryBuilder.newDataEntryBuilder() + .keyMeta(Meta.STRING_META) + .keyData(key) + .valueMeta(Meta.STRING_META) + .valueData(value) + .header("REDIS_COMMAND", command) + .header("PX", px); + + SinkDataEntry sinkDataEntry = builder.buildSinkDataEntry( + 1500L + ); + + assertNotNull(sinkDataEntry); + assertEquals(sinkDataEntry.getKey().convertToString(), "myset"); + assertEquals(sinkDataEntry.getValue().convertToString(), "asldfjsaldjglas"); + assertEquals(sinkDataEntry.getHeaders().findHeader("REDIS_COMMAND").data().convertToString(), "SET"); + assertTrue(sinkDataEntry.getHeaders().toMap().get("PX").data().convertToLong() == 360000); + } + + /** + * simple test for table data + */ + @Test + public void testTable() { + // construct table meta + Meta tableMeta = MetaBuilder.struct() + .field("id", Meta.INT64_META) + .field("name", Meta.STRING_META) + .field("age", Meta.INT16_META) + .field("score", Meta.INT32_META) + .field("isNB", Meta.BOOLEAN_META) + .build(); + + // construct sourceDataEntry + DataEntryBuilder builder = + DataEntryBuilder.newDataEntryBuilder(tableMeta) + .valueData("id", 1L) + .valueData("name", "小红") + .valueData("age", (short)17) + .valueData("score", 99) + .valueData("isNB", true); + + SinkDataEntry sinkDataEntry = builder.buildSinkDataEntry(1500L); + + //System.out.println(sinkDataEntry); + assertNotNull(sinkDataEntry); + assertTrue(sinkDataEntry.getValue().convertToStruct().getInt64("id") == 1L); + assertEquals(sinkDataEntry.getValue().convertToStruct().getString("name"), "小红"); + assertTrue(sinkDataEntry.getValue().convertToStruct().getInt16("age") == (short)17); + assertTrue(sinkDataEntry.getValue().convertToStruct().getInt32("score") == 99); + assertTrue(sinkDataEntry.getValue().convertToStruct().getBoolean("isNB")); + } + + /** + * test data for type of struct + * + * @return + */ + @Test + public void testStruct() { + // 1. construct meta + + DataEntryBuilder structDataEntry = new DataEntryBuilder( + Meta.STRING_META, + MetaBuilder.struct() + .name("myStruct") + .field("field_string", Meta.STRING_META) + .field("field_int32", Meta.INT32_META) + .parameter("parameter", "sadfsadf") + .build() + ); + + // 2. construct data + + structDataEntry + .queue("last_queue") + .shardingKey("shardingkey") + .timestamp(System.currentTimeMillis()) + .entryType(EntryType.UPDATE) + .keyData("schema_data_lalala") + .valueData("field_string", "nihao") + .valueData("field_int32", 321) + .header("int_header", 1) + ; + + // 3. construct dataEntry + + SinkDataEntry sinkDataEntry = structDataEntry.buildSinkDataEntry(1500L); + + //System.out.println(sinkDataEntry); + assertNotNull(sinkDataEntry); + } + +} diff --git a/connector/src/main/test/io/openmessaging/connector/api/data/SourceDataEntryTest.java b/connector/src/main/test/io/openmessaging/connector/api/data/SourceDataEntryTest.java new file mode 100644 index 0000000..87316bf --- /dev/null +++ b/connector/src/main/test/io/openmessaging/connector/api/data/SourceDataEntryTest.java @@ -0,0 +1,128 @@ +package io.openmessaging.connector.api.data; + + +import java.nio.ByteBuffer; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class SourceDataEntryTest { + + /** + * simple test for kv data + * SET myset asldfjsaldjglas PX 360000 + */ + @Test + public void testKV(){ + String command = "SET"; + String key = "myset"; + String value = "asldfjsaldjglas"; + long px = 360000; + + + DataEntryBuilder builder = + DataEntryBuilder.newDataEntryBuilder() + .keyMeta(Meta.STRING_META) + .keyData(key) + .valueMeta(Meta.STRING_META) + .valueData(value) + .header("REDIS_COMMAND", command) + .header("PX", px); + + SourceDataEntry sourceDataEntry = builder.buildSourceDataEntry( + ByteBuffer.wrap("partition1".getBytes()), + ByteBuffer.wrap("1098".getBytes()) + ); + + assertNotNull(sourceDataEntry); + assertEquals(sourceDataEntry.getKey().convertToString(), "myset"); + assertEquals(sourceDataEntry.getValue().convertToString(), "asldfjsaldjglas"); + assertEquals(sourceDataEntry.getHeaders().findHeader("REDIS_COMMAND").data().convertToString(), "SET"); + assertTrue(sourceDataEntry.getHeaders().toMap().get("PX").data().convertToLong() == 360000); + //System.out.println(sourceDataEntry); + } + + /** + * simple test for table data + */ + @Test + public void testTable(){ + // construct table meta + Meta tableMeta = MetaBuilder.struct() + .field("id", Meta.INT64_META) + .field("name", Meta.STRING_META) + .field("age", Meta.INT16_META) + .field("score", Meta.INT32_META) + .field("isNB", Meta.BOOLEAN_META) + .build(); + + // construct sourceDataEntry + DataEntryBuilder builder = + DataEntryBuilder.newDataEntryBuilder(tableMeta) + .valueData("id", 1L) + .valueData("name", "小红") + .valueData("age", (short)17) + .valueData("score", 99) + .valueData("isNB", true); + + SourceDataEntry sourceDataEntry = builder.buildSourceDataEntry( + ByteBuffer.wrap("partition1".getBytes()), + ByteBuffer.wrap("1098".getBytes()) + ); + + assertNotNull(sourceDataEntry); + assertNotNull(sourceDataEntry); + assertTrue(sourceDataEntry.getValue().convertToStruct().getInt64("id") == 1L); + assertEquals(sourceDataEntry.getValue().convertToStruct().getString("name"), "小红"); + assertTrue(sourceDataEntry.getValue().convertToStruct().getInt16("age") == (short)17); + assertTrue(sourceDataEntry.getValue().convertToStruct().getInt32("score") == 99); + assertTrue(sourceDataEntry.getValue().convertToStruct().getBoolean("isNB")); + //System.out.println(sourceDataEntry); + } + + /** + * test data for type of struct + * + * @return + */ + @Test + public void testStruct(){ + // 1. construct meta + + DataEntryBuilder structDataEntry = new DataEntryBuilder( + Meta.STRING_META, + MetaBuilder.struct() + .name("myStruct") + .field("field_string", Meta.STRING_META) + .field("field_int32", Meta.INT32_META) + .parameter("parameter", "sadfsadf") + .build() + ); + + // 2. construct data + + structDataEntry + .queue("last_queue") + .shardingKey("shardingkey") + .timestamp(System.currentTimeMillis()) + .entryType(EntryType.UPDATE) + .keyData("schema_data_lalala") + .valueData("field_string", "nihao") + .valueData("field_int32", 321) + .header("int_header", 1) + ; + + // 3. construct dataEntry + + SourceDataEntry sourceDataEntry = structDataEntry.buildSourceDataEntry(null, null); + + assertNotNull(sourceDataEntry); + //System.out.println(sourceDataEntry); + } + + + +} diff --git a/pom.xml b/pom.xml index 70f4de7..4c2b46c 100644 --- a/pom.xml +++ b/pom.xml @@ -138,6 +138,12 @@ openmessaging-api 0.3.1-alpha + + junit + junit + 4.12 + test + \ No newline at end of file