blob: 7e3d87691a090bb6c9e2b8ac4176df5a22e83232 [file] [log] [blame]
/*
* Copyright 2017 HugeGraph Authors
*
* 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 com.baidu.hugegraph.serializer.direct.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import com.baidu.hugegraph.loader.constant.Constants;
import com.baidu.hugegraph.loader.source.AbstractSource;
import com.baidu.hugegraph.loader.source.InputSource;
import com.baidu.hugegraph.loader.source.file.FileSource;
import com.baidu.hugegraph.loader.source.file.ListFormat;
import com.baidu.hugegraph.structure.constant.Cardinality;
import com.baidu.hugegraph.structure.constant.DataType;
import com.baidu.hugegraph.structure.schema.PropertyKey;
import com.baidu.hugegraph.util.DateUtil;
import com.baidu.hugegraph.util.E;
import com.baidu.hugegraph.util.InsertionOrderUtil;
import com.baidu.hugegraph.util.ReflectionUtil;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
public final class DataTypeUtil {
private static final Set<String> ACCEPTABLE_TRUE = ImmutableSet.of(
"true", "1", "yes", "y"
);
private static final Set<String> ACCEPTABLE_FALSE = ImmutableSet.of(
"false", "0", "no", "n"
);
public static boolean isSimpleValue(Object value) {
if (value == null) {
return false;
}
return ReflectionUtil.isSimpleType(value.getClass());
}
public static Object convert(Object value, PropertyKey propertyKey, InputSource source) {
E.checkArgumentNotNull(value,
"The value to be converted can't be null");
String key = propertyKey.name();
DataType dataType = propertyKey.dataType();
Cardinality cardinality = propertyKey.cardinality();
switch (cardinality) {
case SINGLE:
return parseSingleValue(key, value, dataType, source);
case SET:
case LIST:
return parseMultiValues(key, value, dataType, cardinality, source);
default:
throw new AssertionError(String.format(
"Unsupported cardinality: '%s'", cardinality));
}
}
@SuppressWarnings("unchecked")
public static List<Object> splitField(String key, Object rawColumnValue, InputSource source) {
E.checkArgument(rawColumnValue != null,
"The value to be split can't be null");
if (rawColumnValue instanceof Collection) {
return (List<Object>) rawColumnValue;
}
// TODO: Seems a bit violent
String rawValue = rawColumnValue.toString();
return split(key, rawValue, source);
}
public static long parseNumber(String key, Object rawValue) {
if (rawValue instanceof Number) {
return ((Number) rawValue).longValue();
} else if (rawValue instanceof String) {
// trim() is a little time-consuming
return parseLong(((String) rawValue).trim());
}
throw new IllegalArgumentException(String.format(
"The value(key='%s') must can be casted to Long, " +
"but got '%s'(%s)", key, rawValue, rawValue.getClass().getName()));
}
public static UUID parseUUID(String key, Object rawValue) {
if (rawValue instanceof UUID) {
return (UUID) rawValue;
} else if (rawValue instanceof String) {
String value = ((String) rawValue).trim();
if (value.contains("-")) {
return UUID.fromString(value);
}
// UUID represented by hex string
E.checkArgument(value.length() == 32, "Invalid UUID value(key='%s') '%s'", key, value);
String high = value.substring(0, 16);
String low = value.substring(16);
return new UUID(Long.parseUnsignedLong(high, 16), Long.parseUnsignedLong(low, 16));
}
throw new IllegalArgumentException(String.format(
"Failed to convert value(key='%s') '%s'(%s) to UUID",
key, rawValue, rawValue.getClass()));
}
private static Object parseSingleValue(String key, Object rawValue,
DataType dataType,
InputSource source) {
// Trim space if raw value is string
Object value = rawValue;
if (rawValue instanceof String) {
value = ((String) rawValue).trim();
}
if (dataType.isNumber()) {
return parseNumber(key, value, dataType);
} else if (dataType.isBoolean()) {
return parseBoolean(key, value);
} else if (dataType.isDate()) {
E.checkState(source instanceof FileSource,
"Only accept FileSource when convert String value " +
"to Date, but got '%s'", source.getClass().getName());
String dateFormat = ((FileSource) source).dateFormat();
String timeZone = ((FileSource) source).timeZone();
return parseDate(key, value, dateFormat, timeZone);
} else if (dataType.isUUID()) {
return parseUUID(key, value);
}
E.checkArgument(checkDataType(key, value, dataType),
"The value(key='%s') '%s'(%s) is not match with " +
"data type %s and can't convert to it",
key, value, value.getClass(), dataType);
return value;
}
/**
* collection format: "obj1,obj2,...,objn" or "[obj1,obj2,...,objn]" ..etc
* TODO: After parsing to json, the order of the collection changed
* in some cases (such as list<date>)
**/
private static Object parseMultiValues(String key, Object values,
DataType dataType,
Cardinality cardinality,
InputSource source) {
// JSON file should not parse again
if (values instanceof Collection &&
checkCollectionDataType(key, (Collection<?>) values, dataType)) {
return values;
}
E.checkState(values instanceof String,
"The value(key='%s') must be String type, " +
"but got '%s'(%s)", key, values);
String rawValue = (String) values;
List<Object> valueColl = split(key, rawValue, source);
Collection<Object> results = cardinality == Cardinality.LIST ?
InsertionOrderUtil.newList() :
InsertionOrderUtil.newSet();
valueColl.forEach(value -> {
results.add(parseSingleValue(key, value, dataType, source));
});
E.checkArgument(checkCollectionDataType(key, results, dataType),
"Not all collection elems %s match with data type %s",
results, dataType);
return results;
}
private static Boolean parseBoolean(String key, Object rawValue) {
if (rawValue instanceof Boolean) {
return (Boolean) rawValue;
}
if (rawValue instanceof String) {
String value = ((String) rawValue).toLowerCase();
if (ACCEPTABLE_TRUE.contains(value)) {
return true;
} else if (ACCEPTABLE_FALSE.contains(value)) {
return false;
} else {
throw new IllegalArgumentException(String.format(
"Failed to convert '%s'(key='%s') to Boolean, " +
"the acceptable boolean strings are %s or %s",
key, rawValue, ACCEPTABLE_TRUE, ACCEPTABLE_FALSE));
}
}
throw new IllegalArgumentException(String.format(
"Failed to convert value(key='%s') '%s'(%s) to Boolean",
key, rawValue, rawValue.getClass()));
}
private static Number parseNumber(String key, Object value,
DataType dataType) {
E.checkState(dataType.isNumber(),
"The target data type must be number");
if (dataType.clazz().isInstance(value)) {
return (Number) value;
}
try {
switch (dataType) {
case BYTE:
return Byte.valueOf(value.toString());
case INT:
return Integer.valueOf(value.toString());
case LONG:
return parseLong(value.toString());
case FLOAT:
return Float.valueOf(value.toString());
case DOUBLE:
return Double.valueOf(value.toString());
default:
throw new AssertionError(String.format(
"Number type only contains Byte, Integer, " +
"Long, Float, Double, but got %s",
dataType.clazz()));
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format(
"Failed to convert value(key=%s) '%s'(%s) to Number",
key, value, value.getClass()), e);
}
}
private static long parseLong(String rawValue) {
if (rawValue.startsWith("-")) {
return Long.parseLong(rawValue);
} else {
return Long.parseUnsignedLong(rawValue);
}
}
private static Date parseDate(String key, Object value,
String dateFormat, String timeZone) {
if (value instanceof Date) {
return (Date) value;
}
if (value instanceof Number) {
return new Date(((Number) value).longValue());
} else if (value instanceof String) {
if (Constants.TIMESTAMP.equals(dateFormat)) {
try {
long timestamp = Long.parseLong((String) value);
return new Date(timestamp);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format(
"Invalid timestamp value '%s'", value));
}
} else {
return DateUtil.parse((String) value, dateFormat, timeZone);
}
}
throw new IllegalArgumentException(String.format(
"Failed to convert value(key='%s') '%s'(%s) to Date",
key, value, value.getClass()));
}
private static List<Object> split(String key, String rawValue,
InputSource source) {
List<Object> valueColl = new ArrayList<>();
if (rawValue.isEmpty()) {
return valueColl;
}
E.checkState(AbstractSource.class.isAssignableFrom(source.getClass()),
"Only accept AbstractSource when parse multi values, " +
"but got '%s'", source.getClass().getName());
ListFormat listFormat = ((AbstractSource) source).listFormat();
E.checkArgumentNotNull(listFormat, "The list_format must be set when " +
"parse list or set values");
String startSymbol = listFormat.startSymbol();
String endSymbol = listFormat.endSymbol();
E.checkArgument(rawValue.length() >=
startSymbol.length() + endSymbol.length(),
"The value(key='%s') '%s' length(%s) must be >= " +
"start symbol '%s' + end symbol '%s' length",
key, rawValue, rawValue.length(),
startSymbol, endSymbol);
E.checkArgument(rawValue.startsWith(startSymbol) &&
rawValue.endsWith(endSymbol),
"The value(key='%s') must start with '%s' and " +
"end with '%s', but got '%s'",
key, startSymbol, endSymbol, rawValue);
rawValue = rawValue.substring(startSymbol.length(),
rawValue.length() - endSymbol.length());
String elemDelimiter = listFormat.elemDelimiter();
Splitter.on(elemDelimiter).split(rawValue).forEach(value -> {
if (!listFormat.ignoredElems().contains(value)) {
valueColl.add(value);
}
});
return valueColl;
}
/**
* Check type of the value valid
*/
private static boolean checkDataType(String key, Object value,
DataType dataType) {
if (value instanceof Number) {
return parseNumber(key, value, dataType) != null;
}
return dataType.clazz().isInstance(value);
}
/**
* Check type of all the values(may be some of list properties) valid
*/
private static boolean checkCollectionDataType(String key,
Collection<?> values,
DataType dataType) {
for (Object value : values) {
if (!checkDataType(key, value, dataType)) {
return false;
}
}
return true;
}
}