/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.presto.metadata;
import com.facebook.presto.Session;
import com.facebook.presto.SystemSessionProperties;
import com.facebook.presto.connector.ConnectorId;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.block.BlockBuilder;
import com.facebook.presto.spi.block.BlockBuilderStatus;
import com.facebook.presto.spi.session.PropertyMetadata;
import com.facebook.presto.spi.type.BigintType;
import com.facebook.presto.spi.type.BooleanType;
import com.facebook.presto.spi.type.DoubleType;
import com.facebook.presto.spi.type.IntegerType;
import com.facebook.presto.spi.type.Type;
import com.facebook.presto.spi.type.VarcharType;
import com.facebook.presto.sql.planner.ParameterRewriter;
import com.facebook.presto.sql.tree.Expression;
import com.facebook.presto.sql.tree.ExpressionTreeRewriter;
import com.facebook.presto.type.ArrayType;
import com.facebook.presto.type.MapType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import io.airlift.json.JsonCodec;
import io.airlift.json.JsonCodecFactory;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_SESSION_PROPERTY;
import static com.facebook.presto.spi.type.TypeUtils.writeNativeValue;
import static com.facebook.presto.sql.planner.ExpressionInterpreter.evaluateConstantExpression;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
public final class SessionPropertyManager
{
private static final JsonCodecFactory JSON_CODEC_FACTORY = new JsonCodecFactory();
private final ConcurrentMap<String, PropertyMetadata<?>> systemSessionProperties = new ConcurrentHashMap<>();
private final ConcurrentMap<ConnectorId, Map<String, PropertyMetadata<?>>> connectorSessionProperties = new ConcurrentHashMap<>();
public SessionPropertyManager()
{
this(new SystemSessionProperties());
}
@Inject
public SessionPropertyManager(SystemSessionProperties systemSessionProperties)
{
this(systemSessionProperties.getSessionProperties());
}
public SessionPropertyManager(List<PropertyMetadata<?>> systemSessionProperties)
{
addSystemSessionProperties(systemSessionProperties);
}
public void addSystemSessionProperties(List<PropertyMetadata<?>> systemSessionProperties)
{
systemSessionProperties
.forEach(this::addSystemSessionProperty);
}
public void addSystemSessionProperty(PropertyMetadata<?> sessionProperty)
{
requireNonNull(sessionProperty, "sessionProperty is null");
checkState(systemSessionProperties.put(sessionProperty.getName(), sessionProperty) == null,
"System session property '%s' are already registered", sessionProperty.getName());
}
public void addConnectorSessionProperties(ConnectorId connectorId, List<PropertyMetadata<?>> properties)
{
requireNonNull(connectorId, "connectorId is null");
requireNonNull(properties, "properties is null");
Map<String, PropertyMetadata<?>> propertiesByName = Maps.uniqueIndex(properties, PropertyMetadata::getName);
checkState(connectorSessionProperties.putIfAbsent(connectorId, propertiesByName) == null, "Session properties for connectorId '%s' are already registered", connectorId);
}
public void removeConnectorSessionProperties(ConnectorId connectorId)
{
connectorSessionProperties.remove(connectorId);
}
public Optional<PropertyMetadata<?>> getSystemSessionPropertyMetadata(String name)
{
requireNonNull(name, "name is null");
return Optional.ofNullable(systemSessionProperties.get(name));
}
public Optional<PropertyMetadata<?>> getConnectorSessionPropertyMetadata(ConnectorId connectorId, String propertyName)
{
requireNonNull(connectorId, "connectorId is null");
requireNonNull(propertyName, "propertyName is null");
Map<String, PropertyMetadata<?>> properties = connectorSessionProperties.get(connectorId);
if (properties == null || properties.isEmpty()) {
throw new PrestoException(INVALID_SESSION_PROPERTY, "Unknown connector " + connectorId);
}
return Optional.ofNullable(properties.get(propertyName));
}
public List<SessionPropertyValue> getAllSessionProperties(Session session, Map<String, ConnectorId> catalogs)
{
requireNonNull(session, "session is null");
ImmutableList.Builder<SessionPropertyValue> sessionPropertyValues = ImmutableList.builder();
Map<String, String> systemProperties = session.getSystemProperties();
for (PropertyMetadata<?> property : new TreeMap<>(systemSessionProperties).values()) {
String defaultValue = firstNonNull(property.getDefaultValue(), "").toString();
String value = systemProperties.getOrDefault(property.getName(), defaultValue);
sessionPropertyValues.add(new SessionPropertyValue(
value,
defaultValue,
property.getName(),
Optional.empty(),
property.getName(),
property.getDescription(),
property.getSqlType().getDisplayName(),
property.isHidden()));
}
for (Entry<String, ConnectorId> entry : new TreeMap<>(catalogs).entrySet()) {
String catalog = entry.getKey();
ConnectorId connectorId = entry.getValue();
Map<String, String> connectorProperties = session.getConnectorProperties(connectorId);
for (PropertyMetadata<?> property : new TreeMap<>(connectorSessionProperties.get(connectorId)).values()) {
String defaultValue = firstNonNull(property.getDefaultValue(), "").toString();
String value = connectorProperties.getOrDefault(property.getName(), defaultValue);
sessionPropertyValues.add(new SessionPropertyValue(
value,
defaultValue,
catalog + "." + property.getName(),
Optional.of(catalog),
property.getName(),
property.getDescription(),
property.getSqlType().getDisplayName(),
property.isHidden()));
}
}
return sessionPropertyValues.build();
}
public <T> T decodeSystemPropertyValue(String name, @Nullable String value, Class<T> type)
{
PropertyMetadata<?> property = getSystemSessionPropertyMetadata(name)
.orElseThrow(() -> new PrestoException(INVALID_SESSION_PROPERTY, "Unknown session property " + name));
return decodePropertyValue(name, value, type, property);
}
public <T> T decodeCatalogPropertyValue(ConnectorId connectorId, String catalogName, String propertyName, @Nullable String propertyValue, Class<T> type)
{
String fullPropertyName = catalogName + "." + propertyName;
PropertyMetadata<?> property = getConnectorSessionPropertyMetadata(connectorId, propertyName)
.orElseThrow(() -> new PrestoException(INVALID_SESSION_PROPERTY, "Unknown session property " + fullPropertyName));
return decodePropertyValue(fullPropertyName, propertyValue, type, property);
}
public void validateSystemSessionProperty(String propertyName, String propertyValue)
{
PropertyMetadata<?> propertyMetadata = getSystemSessionPropertyMetadata(propertyName)
.orElseThrow(() -> new PrestoException(INVALID_SESSION_PROPERTY, "Unknown session property " + propertyName));
decodePropertyValue(propertyName, propertyValue, propertyMetadata.getJavaType(), propertyMetadata);
}
public void validateCatalogSessionProperty(ConnectorId connectorId, String catalogName, String propertyName, String propertyValue)
{
String fullPropertyName = catalogName + "." + propertyName;
PropertyMetadata<?> propertyMetadata = getConnectorSessionPropertyMetadata(connectorId, propertyName)
.orElseThrow(() -> new PrestoException(INVALID_SESSION_PROPERTY, "Unknown session property " + fullPropertyName));
decodePropertyValue(fullPropertyName, propertyValue, propertyMetadata.getJavaType(), propertyMetadata);
}
private static <T> T decodePropertyValue(String fullPropertyName, @Nullable String propertyValue, Class<T> type, PropertyMetadata<?> metadata)
{
if (metadata.getJavaType() != type) {
throw new PrestoException(INVALID_SESSION_PROPERTY, format("Property %s is type %s, but requested type was %s", fullPropertyName,
metadata.getJavaType().getName(),
type.getName()));
}
if (propertyValue == null) {
return type.cast(metadata.getDefaultValue());
}
Object objectValue = deserializeSessionProperty(metadata.getSqlType(), propertyValue);
try {
return type.cast(metadata.decode(objectValue));
}
catch (PrestoException e) {
throw e;
}
catch (Exception e) {
// the system property decoder can throw any exception
throw new PrestoException(INVALID_SESSION_PROPERTY, format("%s is invalid: %s", fullPropertyName, propertyValue), e);
}
}
public static Object evaluatePropertyValue(Expression expression, Type expectedType, Session session, Metadata metadata, List<Expression> parameters)
{
Expression rewritten = ExpressionTreeRewriter.rewriteWith(new ParameterRewriter(parameters), expression);
Object value = evaluateConstantExpression(rewritten, expectedType, metadata, session, parameters);
// convert to object value type of SQL type
BlockBuilder blockBuilder = expectedType.createBlockBuilder(new BlockBuilderStatus(), 1);
writeNativeValue(expectedType, blockBuilder, value);
Object objectValue = expectedType.getObjectValue(session.toConnectorSession(), blockBuilder, 0);
if (objectValue == null) {
throw new PrestoException(INVALID_SESSION_PROPERTY, "Session property value must not be null");
}
return objectValue;
}
public static String serializeSessionProperty(Type type, Object value)
{
if (value == null) {
throw new PrestoException(INVALID_SESSION_PROPERTY, "Session property can not be null");
}
if (BooleanType.BOOLEAN.equals(type)) {
return value.toString();
}
if (BigintType.BIGINT.equals(type)) {
return value.toString();
}
if (IntegerType.INTEGER.equals(type)) {
return value.toString();
}
if (DoubleType.DOUBLE.equals(type)) {
return value.toString();
}
if (VarcharType.VARCHAR.equals(type)) {
return value.toString();
}
if (type instanceof ArrayType || type instanceof MapType) {
return getJsonCodecForType(type).toJson(value);
}
throw new PrestoException(INVALID_SESSION_PROPERTY, format("Session property type %s is not supported", type));
}
private static Object deserializeSessionProperty(Type type, String value)
{
if (value == null) {
throw new PrestoException(INVALID_SESSION_PROPERTY, "Session property can not be null");
}
if (VarcharType.VARCHAR.equals(type)) {
return value;
}
if (BooleanType.BOOLEAN.equals(type)) {
return Boolean.valueOf(value);
}
if (BigintType.BIGINT.equals(type)) {
return Long.valueOf(value);
}
if (IntegerType.INTEGER.equals(type)) {
return Integer.valueOf(value);
}
if (DoubleType.DOUBLE.equals(type)) {
return Double.valueOf(value);
}
if (type instanceof ArrayType || type instanceof MapType) {
return getJsonCodecForType(type).fromJson(value);
}
throw new PrestoException(INVALID_SESSION_PROPERTY, format("Session property type %s is not supported", type));
}
private static <T> JsonCodec<T> getJsonCodecForType(Type type)
{
if (VarcharType.VARCHAR.equals(type)) {
return (JsonCodec<T>) JSON_CODEC_FACTORY.jsonCodec(String.class);
}
if (BooleanType.BOOLEAN.equals(type)) {
return (JsonCodec<T>) JSON_CODEC_FACTORY.jsonCodec(Boolean.class);
}
if (BigintType.BIGINT.equals(type)) {
return (JsonCodec<T>) JSON_CODEC_FACTORY.jsonCodec(Long.class);
}
if (IntegerType.INTEGER.equals(type)) {
return (JsonCodec<T>) JSON_CODEC_FACTORY.jsonCodec(Integer.class);
}
if (DoubleType.DOUBLE.equals(type)) {
return (JsonCodec<T>) JSON_CODEC_FACTORY.jsonCodec(Double.class);
}
if (type instanceof ArrayType) {
Type elementType = ((ArrayType) type).getElementType();
return (JsonCodec<T>) JSON_CODEC_FACTORY.listJsonCodec(getJsonCodecForType(elementType));
}
if (type instanceof MapType) {
Type keyType = ((MapType) type).getKeyType();
Type valueType = ((MapType) type).getValueType();
return (JsonCodec<T>) JSON_CODEC_FACTORY.mapJsonCodec(getMapKeyType(keyType), getJsonCodecForType(valueType));
}
throw new PrestoException(INVALID_SESSION_PROPERTY, format("Session property type %s is not supported", type));
}
private static Class<?> getMapKeyType(Type type)
{
if (VarcharType.VARCHAR.equals(type)) {
return String.class;
}
if (BooleanType.BOOLEAN.equals(type)) {
return Boolean.class;
}
if (BigintType.BIGINT.equals(type)) {
return Long.class;
}
if (IntegerType.INTEGER.equals(type)) {
return Integer.class;
}
if (DoubleType.DOUBLE.equals(type)) {
return Double.class;
}
throw new PrestoException(INVALID_SESSION_PROPERTY, format("Session property map key type %s is not supported", type));
}
public static class SessionPropertyValue
{
private final String fullyQualifiedName;
private final Optional<String> catalogName;
private final String propertyName;
private final String description;
private final String type;
private final String value;
private final String defaultValue;
private final boolean hidden;
private SessionPropertyValue(String value,
String defaultValue,
String fullyQualifiedName,
Optional<String> catalogName,
String propertyName,
String description,
String type,
boolean hidden)
{
this.fullyQualifiedName = fullyQualifiedName;
this.catalogName = catalogName;
this.propertyName = propertyName;
this.description = description;
this.type = type;
this.value = value;
this.defaultValue = defaultValue;
this.hidden = hidden;
}
public String getFullyQualifiedName()
{
return fullyQualifiedName;
}
public Optional<String> getCatalogName()
{
return catalogName;
}
public String getPropertyName()
{
return propertyName;
}
public String getDescription()
{
return description;
}
public String getType()
{
return type;
}
public String getValue()
{
return value;
}
public String getDefaultValue()
{
return defaultValue;
}
public boolean isHidden()
{
return hidden;
}
}
}