/** * 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 org.apache.drill.exec.store.dfs; import static java.util.Collections.unmodifiableMap; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.drill.common.exceptions.UserException; import org.apache.drill.common.logical.FormatPluginConfig; import org.apache.drill.exec.store.dfs.WorkspaceSchemaFactory.TableInstance; import org.apache.drill.exec.store.dfs.WorkspaceSchemaFactory.TableParamDef; import org.apache.drill.exec.store.dfs.WorkspaceSchemaFactory.TableSignature; import org.slf4j.Logger; import com.fasterxml.jackson.annotation.JsonTypeName; /** * Describes the options for a format plugin * extracted from the FormatPluginConfig subclass */ final class FormatPluginOptionsDescriptor { private static final Logger logger = org.slf4j.LoggerFactory.getLogger(FormatPluginOptionsDescriptor.class); final Class<? extends FormatPluginConfig> pluginConfigClass; final String typeName; private final Map<String, TableParamDef> functionParamsByName; /** * Uses reflection to extract options based on the fields of the provided config class * ("List extensions" field is ignored, pending removal, Char is turned into String) * The class must be annotated with {@code @JsonTypeName("type name")} * @param pluginConfigClass the config class we want to extract options from through reflection */ FormatPluginOptionsDescriptor(Class<? extends FormatPluginConfig> pluginConfigClass) { this.pluginConfigClass = pluginConfigClass; Map<String, TableParamDef> paramsByName = new LinkedHashMap<>(); Field[] fields = pluginConfigClass.getDeclaredFields(); // @JsonTypeName("text") JsonTypeName annotation = pluginConfigClass.getAnnotation(JsonTypeName.class); this.typeName = annotation != null ? annotation.value() : null; if (this.typeName != null) { paramsByName.put("type", new TableParamDef("type", String.class)); } for (Field field : fields) { if (Modifier.isStatic(field.getModifiers()) // we want to deprecate this field || (field.getName().equals("extensions") && field.getType() == List.class)) { continue; } Class<?> fieldType = field.getType(); if (fieldType == char.class) { // calcite does not like char type. Just use String and enforce later that length == 1 fieldType = String.class; } paramsByName.put(field.getName(), new TableParamDef(field.getName(), fieldType).optional()); } this.functionParamsByName = unmodifiableMap(paramsByName); } /** * returns the table function signature for this format plugin config class * @param tableName the table for which we want a table function signature * @return the signature */ TableSignature getTableSignature(String tableName) { return new TableSignature(tableName, params()); } /** * @return the parameters extracted from the provided format plugin config class */ private List<TableParamDef> params() { return new ArrayList<>(functionParamsByName.values()); } /** * @return a readable String of the parameters and their names */ String presentParams() { StringBuilder sb = new StringBuilder("("); List<TableParamDef> params = params(); for (int i = 0; i < params.size(); i++) { TableParamDef paramDef = params.get(i); if (i != 0) { sb.append(", "); } sb.append(paramDef.name).append(": ").append(paramDef.type.getSimpleName()); } sb.append(")"); return sb.toString(); } /** * creates an instance of the FormatPluginConfig based on the passed parameters * @param t the signature and the parameters passed to the table function * @return the corresponding config */ FormatPluginConfig createConfigForTable(TableInstance t) { // Per the constructor, the first param is always "type" TableParamDef typeParamDef = t.sig.params.get(0); Object typeParam = t.params.get(0); if (!typeParamDef.name.equals("type") || typeParamDef.type != String.class || !(typeParam instanceof String) || !typeName.equalsIgnoreCase((String)typeParam)) { // if we reach here, there's a bug as all signatures generated start with a type parameter throw UserException.parseError() .message( "This function signature is not supported: %s\n" + "expecting %s", t.presentParams(), this.presentParams()) .addContext("table", t.sig.name) .build(logger); } FormatPluginConfig config; try { config = pluginConfigClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw UserException.parseError(e) .message( "configuration for format of type %s can not be created (class: %s)", this.typeName, pluginConfigClass.getName()) .addContext("table", t.sig.name) .build(logger); } for (int i = 1; i < t.params.size(); i++) { Object param = t.params.get(i); if (param == null) { // when null is passed, we leave the default defined in the config class continue; } if (param instanceof String) { // normalize Java literals, ex: \t, \n, \r param = StringEscapeUtils.unescapeJava((String) param); } TableParamDef paramDef = t.sig.params.get(i); TableParamDef expectedParamDef = this.functionParamsByName.get(paramDef.name); if (expectedParamDef == null || expectedParamDef.type != paramDef.type) { throw UserException.parseError() .message( "The parameters provided are not applicable to the type specified:\n" + "provided: %s\nexpected: %s", t.presentParams(), this.presentParams()) .addContext("table", t.sig.name) .build(logger); } try { Field field = pluginConfigClass.getField(paramDef.name); field.setAccessible(true); if (field.getType() == char.class && param instanceof String) { String stringParam = (String) param; if (stringParam.length() != 1) { throw UserException.parseError() .message("Expected single character but was String: %s", stringParam) .addContext("table", t.sig.name) .addContext("parameter", paramDef.name) .build(logger); } param = stringParam.charAt(0); } field.set(config, param); } catch (IllegalAccessException | NoSuchFieldException | SecurityException e) { throw UserException.parseError(e) .message("can not set value %s to parameter %s: %s", param, paramDef.name, paramDef.type) .addContext("table", t.sig.name) .addContext("parameter", paramDef.name) .build(logger); } } return config; } @Override public String toString() { return "OptionsDescriptor [pluginConfigClass=" + pluginConfigClass + ", typeName=" + typeName + ", functionParamsByName=" + functionParamsByName + "]"; } }