/* * Licensed to Crate under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. Crate 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. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial * agreement. */ package io.crate.analyze.expressions; import com.google.common.base.Preconditions; import io.crate.analyze.AnalyzedColumnDefinition; import io.crate.analyze.symbol.DynamicReference; import io.crate.analyze.symbol.Literal; import io.crate.analyze.symbol.Symbol; import io.crate.analyze.symbol.format.SymbolFormatter; import io.crate.analyze.symbol.format.SymbolPrinter; import io.crate.exceptions.ColumnUnknownException; import io.crate.exceptions.ColumnValidationException; import io.crate.exceptions.ConversionException; import io.crate.metadata.ColumnIdent; import io.crate.metadata.Reference; import io.crate.metadata.Schemas; import io.crate.metadata.doc.DocTableInfo; import io.crate.metadata.table.ColumnPolicy; import io.crate.metadata.table.TableInfo; import io.crate.types.ArrayType; import io.crate.types.DataType; import io.crate.types.DataTypes; import io.crate.types.ObjectType; import java.util.Locale; import java.util.Map; public class ValueNormalizer { private Schemas schemas; public ValueNormalizer(Schemas schemas) { this.schemas = schemas; } /** * normalize and validate given value according to the corresponding {@link io.crate.metadata.Reference} * * @param valueSymbol the value to normalize, might be anything from {@link io.crate.metadata.Scalar} to {@link io.crate.analyze.symbol.Literal} * @param reference the reference to which the value has to comply in terms of type-compatibility * @return the normalized Symbol, should be a literal * @throws io.crate.exceptions.ColumnValidationException */ public Symbol normalizeInputForReference(Symbol valueSymbol, Reference reference) { assert valueSymbol != null : "valueSymbol must not be null"; DataType<?> targetType = getTargetType(valueSymbol, reference); if (!(valueSymbol instanceof Literal)) { return ExpressionAnalyzer.castIfNeededOrFail(valueSymbol, targetType); } Literal literal = (Literal) valueSymbol; try { literal = Literal.convert(literal, reference.valueType()); } catch (ConversionException e) { throw new ColumnValidationException( reference.ident().columnIdent().name(), String.format(Locale.ENGLISH, "%s cannot be cast to type %s", SymbolPrinter.INSTANCE.printSimple(valueSymbol), reference.valueType().getName())); } Object value = literal.value(); if (value == null) { return literal; } try { if (targetType == DataTypes.OBJECT) { //noinspection unchecked normalizeObjectValue((Map) value, reference); } else if (isObjectArray(targetType)) { normalizeObjectArrayValue((Object[]) value, reference); } } catch (ConversionException e) { throw new ColumnValidationException( reference.ident().columnIdent().name(), SymbolFormatter.format( "\"%s\" has a type that can't be implicitly cast to that of \"%s\" (" + reference.valueType().getName() + ")", literal, reference )); } return literal; } private static DataType<?> getTargetType(Symbol valueSymbol, Reference reference) { DataType<?> targetType; if (reference instanceof DynamicReference) { targetType = valueSymbol.valueType(); ((DynamicReference) reference).valueType(targetType); } else { targetType = reference.valueType(); } return targetType; } @SuppressWarnings("unchecked") private void normalizeObjectValue(Map<String, Object> value, Reference info) { for (Map.Entry<String, Object> entry : value.entrySet()) { AnalyzedColumnDefinition.validateName(entry.getKey()); ColumnIdent nestedIdent = ColumnIdent.getChild(info.ident().columnIdent(), entry.getKey()); TableInfo tableInfo = schemas.getTableInfo(info.ident().tableIdent()); Reference nestedInfo = tableInfo.getReference(nestedIdent); if (nestedInfo == null) { if (info.columnPolicy() == ColumnPolicy.IGNORED) { continue; } DynamicReference dynamicReference = null; if (tableInfo instanceof DocTableInfo) { dynamicReference = ((DocTableInfo) tableInfo).getDynamic(nestedIdent, true); } if (dynamicReference == null) { throw new ColumnUnknownException(nestedIdent.sqlFqn()); } DataType type = DataTypes.guessType(entry.getValue()); if (type == null) { throw new ColumnValidationException(info.ident().columnIdent().sqlFqn(), "Invalid value"); } dynamicReference.valueType(type); nestedInfo = dynamicReference; } else { if (entry.getValue() == null) { continue; } } if (nestedInfo.valueType() == DataTypes.OBJECT && entry.getValue() instanceof Map) { normalizeObjectValue((Map<String, Object>) entry.getValue(), nestedInfo); } else if (isObjectArray(nestedInfo.valueType()) && entry.getValue() instanceof Object[]) { normalizeObjectArrayValue((Object[]) entry.getValue(), nestedInfo); } else { entry.setValue(normalizePrimitiveValue(entry.getValue(), nestedInfo)); } } } private static boolean isObjectArray(DataType type) { return type.id() == ArrayType.ID && ((ArrayType) type).innerType().id() == ObjectType.ID; } private void normalizeObjectArrayValue(Object[] value, Reference arrayInfo) { for (Object arrayItem : value) { Preconditions.checkArgument(arrayItem instanceof Map, "invalid value for object array type"); // return value not used and replaced in value as arrayItem is a map that is mutated //noinspection unchecked normalizeObjectValue((Map<String, Object>) arrayItem, arrayInfo); } } private static Object normalizePrimitiveValue(Object primitiveValue, Reference info) { if (info.valueType().equals(DataTypes.STRING) && primitiveValue instanceof String) { return primitiveValue; } try { return info.valueType().value(primitiveValue); } catch (Exception e) { throw new ColumnValidationException(info.ident().columnIdent().sqlFqn(), String.format(Locale.ENGLISH, "Invalid %s", info.valueType().getName()) ); } } }