2016-07-06 09:47:55 +00:00
|
|
|
#include <DB/Functions/IFunction.h>
|
2016-08-04 15:06:27 +00:00
|
|
|
#include <DB/Columns/ColumnConst.h>
|
2016-07-06 09:47:55 +00:00
|
|
|
#include <DB/Columns/ColumnNullable.h>
|
|
|
|
#include <DB/DataTypes/DataTypeNull.h>
|
|
|
|
#include <DB/DataTypes/DataTypeNullable.h>
|
|
|
|
#include <DB/Interpreters/ExpressionActions.h>
|
|
|
|
|
|
|
|
namespace DB
|
|
|
|
{
|
|
|
|
|
|
|
|
namespace
|
|
|
|
{
|
|
|
|
|
2016-08-11 16:47:28 +00:00
|
|
|
/// Suppose a function which has no special support for nullable arguments
|
|
|
|
/// has been called with arguments, one or more of them being nullable.
|
|
|
|
/// Then the method below endows the result, which is nullable, with a null
|
|
|
|
/// byte map that is determined by OR-ing the null byte maps of the nullable
|
|
|
|
/// arguments.
|
2016-08-11 00:17:30 +00:00
|
|
|
void createNullValuesByteMap(Block & block, const ColumnNumbers & args, size_t result)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-10 19:12:29 +00:00
|
|
|
ColumnNullable & res_col = static_cast<ColumnNullable &>(*block.unsafeGetByPosition(result).column);
|
2016-07-06 09:47:55 +00:00
|
|
|
|
2016-08-11 00:17:30 +00:00
|
|
|
for (const auto & arg : args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-11 00:17:30 +00:00
|
|
|
const ColumnWithTypeAndName & elem = block.unsafeGetByPosition(arg);
|
2016-08-15 11:14:29 +00:00
|
|
|
if (elem.column->isNullable())
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-11 00:17:30 +00:00
|
|
|
const ColumnNullable & nullable_col = static_cast<const ColumnNullable &>(*elem.column);
|
|
|
|
res_col.updateNullValuesByteMap(nullable_col);
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
/// In a set of objects (columns of a block / set of columns / set of data types),
|
|
|
|
/// are there any "special" (i.e. nullable or null) objects?
|
|
|
|
enum class Category
|
2016-08-05 07:49:56 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
/// No nullable objects. No null objects.
|
|
|
|
IS_ORDINARY = 0,
|
|
|
|
/// At least one nullable object. No null objects.
|
|
|
|
IS_NULLABLE,
|
|
|
|
/// At least one null object.
|
|
|
|
IS_NULL
|
|
|
|
};
|
|
|
|
|
|
|
|
/// Check if a block contains at least one special column, in the sense
|
|
|
|
/// defined above, among the specified columns.
|
|
|
|
Category blockHasSpecialColumns(const Block & block, const ColumnNumbers & args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
bool found_nullable = false;
|
|
|
|
bool found_null = false;
|
2016-07-06 09:47:55 +00:00
|
|
|
|
|
|
|
for (const auto & arg : args)
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
const auto & elem = block.unsafeGetByPosition(arg);
|
|
|
|
|
|
|
|
if (!found_null && elem.column->isNull())
|
|
|
|
{
|
|
|
|
found_null = true;
|
|
|
|
break;
|
|
|
|
}
|
2016-08-15 13:30:37 +00:00
|
|
|
else if (!found_nullable && elem.column->isNullable())
|
2016-08-15 11:14:29 +00:00
|
|
|
found_nullable = true;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (found_null)
|
|
|
|
return Category::IS_NULL;
|
|
|
|
else if (found_nullable)
|
|
|
|
return Category::IS_NULLABLE;
|
|
|
|
else
|
|
|
|
return Category::IS_ORDINARY;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
/// Check if at least one column is special in the sense defined above.
|
|
|
|
Category hasSpecialColumns(const ColumnsWithTypeAndName & args)
|
2016-08-05 07:49:56 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
bool found_nullable = false;
|
|
|
|
bool found_null = false;
|
2016-08-05 07:49:56 +00:00
|
|
|
|
2016-07-06 09:47:55 +00:00
|
|
|
for (const auto & arg : args)
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
if (!found_null && arg.type->isNull())
|
|
|
|
{
|
|
|
|
found_null = true;
|
|
|
|
break;
|
|
|
|
}
|
2016-08-15 13:30:37 +00:00
|
|
|
else if (!found_nullable && arg.type->isNullable())
|
2016-08-15 11:14:29 +00:00
|
|
|
found_nullable = true;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (found_null)
|
|
|
|
return Category::IS_NULL;
|
|
|
|
else if (found_nullable)
|
|
|
|
return Category::IS_NULLABLE;
|
|
|
|
else
|
|
|
|
return Category::IS_ORDINARY;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
/// Check if at least one data type is special in the sense defined above.
|
|
|
|
Category hasSpecialDataTypes(const DataTypes & args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
bool found_nullable = false;
|
|
|
|
bool found_null = false;
|
|
|
|
|
2016-07-06 09:47:55 +00:00
|
|
|
for (const auto & arg : args)
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
if (!found_null && arg->isNull())
|
|
|
|
{
|
|
|
|
found_null = true;
|
|
|
|
break;
|
|
|
|
}
|
2016-08-15 13:30:37 +00:00
|
|
|
else if (!found_nullable && arg->isNullable())
|
2016-08-15 11:14:29 +00:00
|
|
|
found_nullable = true;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (found_null)
|
|
|
|
return Category::IS_NULL;
|
|
|
|
else if (found_nullable)
|
|
|
|
return Category::IS_NULLABLE;
|
|
|
|
else
|
|
|
|
return Category::IS_ORDINARY;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
/// Turn the specified set of columns into their respective nested columns.
|
|
|
|
ColumnsWithTypeAndName toNestedColumns(const ColumnsWithTypeAndName & args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
|
|
|
ColumnsWithTypeAndName new_args;
|
|
|
|
new_args.reserve(args.size());
|
|
|
|
|
|
|
|
for (const auto & arg : args)
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
if (arg.type->isNullable())
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
|
|
|
auto nullable_col = static_cast<const ColumnNullable *>(arg.column.get());
|
|
|
|
ColumnPtr nested_col = (nullable_col != nullptr) ? nullable_col->getNestedColumn() : nullptr;
|
|
|
|
auto nullable_type = static_cast<const DataTypeNullable *>(arg.type.get());
|
|
|
|
DataTypePtr nested_type = nullable_type->getNestedType();
|
|
|
|
|
|
|
|
new_args.emplace_back(nested_col, nested_type, arg.name);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
new_args.emplace_back(arg.column, arg.type, arg.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new_args;
|
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
/// Turn the specified set of data types into their respective nested data types.
|
|
|
|
DataTypes toNestedDataTypes(const DataTypes & args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
|
|
|
DataTypes new_args;
|
|
|
|
new_args.reserve(args.size());
|
|
|
|
|
|
|
|
for (const auto & arg : args)
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
if (arg->isNullable())
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
|
|
|
auto nullable_type = static_cast<const DataTypeNullable *>(arg.get());
|
|
|
|
DataTypePtr nested_type = nullable_type->getNestedType();
|
|
|
|
new_args.push_back(nested_type);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
new_args.push_back(arg);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new_args;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
DataTypePtr IFunction::getReturnType(const DataTypes & arguments) const
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
auto category = hasSpecialDataTypes(arguments);
|
2016-07-06 09:47:55 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (category == Category::IS_ORDINARY)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
}
|
|
|
|
else if (category == Category::IS_NULL)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
return std::make_shared<DataTypeNull>();
|
|
|
|
}
|
|
|
|
else if (category == Category::IS_NULLABLE)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
{
|
|
|
|
const DataTypes new_args = toNestedDataTypes(arguments);
|
|
|
|
return getReturnTypeImpl(new_args);
|
|
|
|
}
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
else
|
2016-08-15 11:14:29 +00:00
|
|
|
throw Exception{"IFunction: internal error", ErrorCodes::LOGICAL_ERROR};
|
|
|
|
|
|
|
|
return getReturnTypeImpl(arguments);
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void IFunction::getReturnTypeAndPrerequisites(
|
|
|
|
const ColumnsWithTypeAndName & arguments,
|
|
|
|
DataTypePtr & out_return_type,
|
|
|
|
std::vector<ExpressionAction> & out_prerequisites)
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
auto category = hasSpecialColumns(arguments);
|
|
|
|
|
|
|
|
if (category == Category::IS_ORDINARY)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
|
|
|
}
|
2016-08-15 11:14:29 +00:00
|
|
|
else if (category == Category::IS_NULL)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
if (!hasSpecialSupportForNulls())
|
2016-08-15 13:30:37 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
out_return_type = std::make_shared<DataTypeNull>();
|
2016-08-15 13:30:37 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-08-15 11:14:29 +00:00
|
|
|
}
|
|
|
|
else if (category == Category::IS_NULLABLE)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
{
|
|
|
|
const ColumnsWithTypeAndName new_args = toNestedColumns(arguments);
|
|
|
|
getReturnTypeAndPrerequisitesImpl(new_args, out_return_type, out_prerequisites);
|
|
|
|
out_return_type = std::make_shared<DataTypeNullable>(out_return_type);
|
2016-08-15 13:30:37 +00:00
|
|
|
return;
|
2016-08-15 11:14:29 +00:00
|
|
|
}
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
else
|
2016-08-15 11:14:29 +00:00
|
|
|
throw Exception{"IFunction: internal error", ErrorCodes::LOGICAL_ERROR};
|
|
|
|
|
|
|
|
getReturnTypeAndPrerequisitesImpl(arguments, out_return_type, out_prerequisites);
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void IFunction::getLambdaArgumentTypes(DataTypes & arguments) const
|
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
auto category = hasSpecialDataTypes(arguments);
|
2016-07-06 09:47:55 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (category == Category::IS_ORDINARY)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
}
|
|
|
|
else if (category == Category::IS_NULL)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if (category == Category::IS_NULLABLE)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
{
|
|
|
|
DataTypes new_args = toNestedDataTypes(arguments);
|
|
|
|
getLambdaArgumentTypesImpl(new_args);
|
|
|
|
arguments = std::move(new_args);
|
2016-08-15 13:30:37 +00:00
|
|
|
return;
|
2016-08-15 11:14:29 +00:00
|
|
|
}
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
else
|
2016-08-15 11:14:29 +00:00
|
|
|
throw Exception{"IFunction: internal error", ErrorCodes::LOGICAL_ERROR};
|
|
|
|
|
|
|
|
getLambdaArgumentTypesImpl(arguments);
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
void IFunction::execute(Block & block, const ColumnNumbers & args, size_t result)
|
2016-08-10 19:12:29 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
auto strategy = chooseStrategy(block, args);
|
|
|
|
Block processed_block = preProcessBlock(strategy, block, args);
|
2016-08-10 19:12:29 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (strategy != RETURN_NULL)
|
2016-08-10 19:12:29 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
Block & src = processed_block ? processed_block : block;
|
|
|
|
executeImpl(src, args, result);
|
|
|
|
}
|
2016-08-10 19:12:29 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
postProcessResult(strategy, block, processed_block, args, result);
|
|
|
|
}
|
2016-08-10 19:12:29 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
void IFunction::execute(Block & block, const ColumnNumbers & args, const ColumnNumbers & prerequisites, size_t result)
|
|
|
|
{
|
|
|
|
auto strategy = chooseStrategy(block, args);
|
|
|
|
Block processed_block = preProcessBlock(strategy, block, args);
|
2016-08-10 19:12:29 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
if (strategy != RETURN_NULL)
|
|
|
|
{
|
|
|
|
Block & src = processed_block ? processed_block : block;
|
|
|
|
executeImpl(src, args, prerequisites, result);
|
2016-08-10 19:12:29 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
postProcessResult(strategy, block, processed_block, args, result);
|
2016-08-10 19:12:29 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
IFunction::Strategy IFunction::chooseStrategy(const Block & block, const ColumnNumbers & args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
auto category = blockHasSpecialColumns(block, args);
|
|
|
|
|
|
|
|
if (category == Category::IS_ORDINARY)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
|
|
|
}
|
2016-08-15 11:14:29 +00:00
|
|
|
else if (category == Category::IS_NULL)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
return RETURN_NULL;
|
|
|
|
}
|
|
|
|
else if (category == Category::IS_NULLABLE)
|
|
|
|
{
|
|
|
|
if (!hasSpecialSupportForNulls())
|
|
|
|
return PROCESS_NULLABLE_COLUMNS;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
throw Exception{"IFunction: internal error", ErrorCodes::LOGICAL_ERROR};
|
|
|
|
|
|
|
|
return DIRECTLY_EXECUTE;
|
|
|
|
}
|
2016-07-06 09:47:55 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
Block IFunction::preProcessBlock(Strategy strategy, const Block & block, const ColumnNumbers & args)
|
|
|
|
{
|
|
|
|
if (strategy == DIRECTLY_EXECUTE)
|
|
|
|
return {};
|
|
|
|
else if (strategy == RETURN_NULL)
|
|
|
|
return {};
|
|
|
|
else if (strategy == PROCESS_NULLABLE_COLUMNS)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-12 15:29:45 +00:00
|
|
|
/// Run the function on a block whose nullable columns have been replaced
|
|
|
|
/// with their respective nested columns.
|
2016-08-15 11:14:29 +00:00
|
|
|
return createBlockWithNestedColumns(block, args);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
throw Exception{"IFunction: internal error", ErrorCodes::LOGICAL_ERROR};
|
|
|
|
}
|
2016-08-12 15:22:28 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
void IFunction::postProcessResult(Strategy strategy, Block & block, const Block & processed_block,
|
|
|
|
const ColumnNumbers & args, size_t result)
|
|
|
|
{
|
|
|
|
if (strategy == DIRECTLY_EXECUTE)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
else if (strategy == RETURN_NULL)
|
|
|
|
{
|
|
|
|
/// We have found at least one NULL argument. Therefore we return NULL.
|
|
|
|
ColumnWithTypeAndName & dest_col = block.getByPosition(result);
|
|
|
|
dest_col.column = std::make_shared<ColumnNull>(block.rowsInFirstColumn(), Null());
|
|
|
|
}
|
|
|
|
else if (strategy == PROCESS_NULLABLE_COLUMNS)
|
|
|
|
{
|
2016-08-12 15:29:45 +00:00
|
|
|
/// Initialize the result column.
|
2016-08-15 11:14:29 +00:00
|
|
|
const ColumnWithTypeAndName & source_col = processed_block.getByPosition(result);
|
2016-07-06 09:47:55 +00:00
|
|
|
ColumnWithTypeAndName & dest_col = block.getByPosition(result);
|
|
|
|
dest_col.column = std::make_shared<ColumnNullable>(source_col.column);
|
2016-08-12 15:22:28 +00:00
|
|
|
|
2016-08-12 15:29:45 +00:00
|
|
|
/// Make a null map for the result.
|
2016-08-10 19:12:29 +00:00
|
|
|
ColumnNullable & nullable_col = static_cast<ColumnNullable &>(*dest_col.column);
|
2016-08-11 00:17:30 +00:00
|
|
|
nullable_col.getNullValuesByteMap() = std::make_shared<ColumnUInt8>(dest_col.column->size(), 0);
|
2016-08-15 11:14:29 +00:00
|
|
|
createNullValuesByteMap(block, args, result);
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
else
|
2016-08-15 11:14:29 +00:00
|
|
|
throw Exception{"IFunction: internal error", ErrorCodes::LOGICAL_ERROR};
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
/// Return a copy of a given block in which the specified columns are replaced by
|
|
|
|
/// their respective nested columns if they are nullable.
|
|
|
|
Block IFunction::createBlockWithNestedColumns(const Block & block, ColumnNumbers args)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
std::sort(args.begin(), args.end());
|
2016-07-06 09:47:55 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
Block res;
|
2016-08-05 11:31:55 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
size_t j = 0;
|
|
|
|
for (size_t i = 0; i < block.columns(); ++i)
|
2016-07-06 09:47:55 +00:00
|
|
|
{
|
2016-08-15 11:14:29 +00:00
|
|
|
const auto & col = block.unsafeGetByPosition(i);
|
2016-08-15 12:42:12 +00:00
|
|
|
bool is_inserted = false;
|
2016-08-15 11:14:29 +00:00
|
|
|
|
2016-08-15 12:42:12 +00:00
|
|
|
if (i == args[j])
|
2016-08-15 11:14:29 +00:00
|
|
|
{
|
2016-08-15 12:42:12 +00:00
|
|
|
++j;
|
2016-08-15 11:14:29 +00:00
|
|
|
|
2016-08-15 12:42:12 +00:00
|
|
|
if (col.column->isNullable())
|
|
|
|
{
|
|
|
|
auto nullable_col = static_cast<const ColumnNullable *>(col.column.get());
|
|
|
|
ColumnPtr nested_col = nullable_col->getNestedColumn();
|
|
|
|
|
|
|
|
auto nullable_type = static_cast<const DataTypeNullable *>(col.type.get());
|
|
|
|
DataTypePtr nested_type = nullable_type->getNestedType();
|
2016-08-15 11:14:29 +00:00
|
|
|
|
2016-08-15 12:42:12 +00:00
|
|
|
res.insert(i, {nested_col, nested_type, col.name});
|
|
|
|
|
|
|
|
is_inserted = true;
|
|
|
|
}
|
2016-08-15 11:14:29 +00:00
|
|
|
}
|
2016-08-15 12:42:12 +00:00
|
|
|
|
|
|
|
if (!is_inserted)
|
2016-08-15 11:14:29 +00:00
|
|
|
res.insert(i, col);
|
|
|
|
}
|
2016-08-05 11:31:55 +00:00
|
|
|
|
2016-08-15 11:14:29 +00:00
|
|
|
return res;
|
2016-07-06 09:47:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|