Commit ea875c21 authored by coderTomato's avatar coderTomato

Merge remote-tracking branch 'origin/dev' into dev

parents b69ac7bd 5c6f8d22
...@@ -49,6 +49,7 @@ Dinky(原 Dlink): ...@@ -49,6 +49,7 @@ Dinky(原 Dlink):
| | | 新增 选中片段执行 | 0.4.0 | | | | 新增 选中片段执行 | 0.4.0 |
| | | 新增 布局拖拽 | 0.4.0 | | | | 新增 布局拖拽 | 0.4.0 |
| | | 新增 SQL导出 | 0.5.0 | | | | 新增 SQL导出 | 0.5.0 |
| | | 新增 快捷键保存、校验、美化 | 0.5.0 |
| | | 支持 local 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 local 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 standalone 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 standalone 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 yarn session 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 yarn session 模式下 FlinkSQL 提交 | 0.4.0 |
...@@ -56,16 +57,19 @@ Dinky(原 Dlink): ...@@ -56,16 +57,19 @@ Dinky(原 Dlink):
| | | 支持 yarn application 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 yarn application 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 kubernetes session 模式下 FlinkSQL 提交 | 0.5.0 | | | | 支持 kubernetes session 模式下 FlinkSQL 提交 | 0.5.0 |
| | | 支持 kubernetes application 模式下 FlinkSQL 提交 | 0.5.0 | | | | 支持 kubernetes application 模式下 FlinkSQL 提交 | 0.5.0 |
| | | 支持 UDF Java 方言Local模式在线编写、调试、动态加载 | 0.5.0 |
| | Flink 作业 | 支持 yarn application 模式下 Jar 提交 | 0.4.0 | | | Flink 作业 | 支持 yarn application 模式下 Jar 提交 | 0.4.0 |
| | | 支持 k8s application 模式下 Jar 提交 | 0.5.0 | | | | 支持 k8s application 模式下 Jar 提交 | 0.5.0 |
| | | 支持 作业 Cancel | 0.4.0 | | | | 支持 作业 Cancel | 0.4.0 |
| | | 支持 作业 SavePoint 的 Cancel、Stop、Trigger | 0.4.0 | | | | 支持 作业 SavePoint 的 Cancel、Stop、Trigger | 0.4.0 |
| | | 新增 作业自动从 SavePoint 恢复机制(包含最近、最早、指定一次) | 0.4.0 | | | | 新增 作业自动从 SavePoint 恢复机制(包含最近、最早、指定一次) | 0.4.0 |
| | | 新增 UDF java方言代码的开发 | 0.5.0 |
| | Flink 集群 | 支持 查看已注册集群的作业列表与运维 | 0.4.0 | | | Flink 集群 | 支持 查看已注册集群的作业列表与运维 | 0.4.0 |
| | | 新增 自动注册 Yarn 创建的集群 | 0.4.0 | | | | 新增 自动注册 Yarn 创建的集群 | 0.4.0 |
| | SQL | 新增 外部数据源的 SQL 校验 | 0.5.0 | | | SQL | 新增 外部数据源的 SQL 校验 | 0.5.0 |
| | | 新增 外部数据源的 SQL 执行与预览 | 0.5.0 | | | | 新增 外部数据源的 SQL 执行与预览 | 0.5.0 |
| | BI | 新增 折线图的渲染 | 0.5.0 |
| | | 新增 条形图图的渲染 | 0.5.0 |
| | | 新增 饼图的渲染 | 0.5.0 |
| | 元数据 | 新增 查询外部数据源的元数据信息 | 0.4.0 | | | 元数据 | 新增 查询外部数据源的元数据信息 | 0.4.0 |
| | 归档 | 新增 执行与提交历史 | 0.4.0 | | | 归档 | 新增 执行与提交历史 | 0.4.0 |
| 运维中心 | 暂无 | 暂无 | 0.4.0 | | 运维中心 | 暂无 | 暂无 | 0.4.0 |
......
...@@ -24,4 +24,6 @@ public interface TaskService extends ISuperService<Task> { ...@@ -24,4 +24,6 @@ public interface TaskService extends ISuperService<Task> {
List<Task> listFlinkSQLEnv(); List<Task> listFlinkSQLEnv();
String exportSql(Integer id); String exportSql(Integer id);
Task getUDFByClassName(String className);
} }
...@@ -88,6 +88,7 @@ public class CatalogueServiceImpl extends SuperServiceImpl<CatalogueMapper, Cata ...@@ -88,6 +88,7 @@ public class CatalogueServiceImpl extends SuperServiceImpl<CatalogueMapper, Cata
}else{ }else{
Task task = new Task(); Task task = new Task();
task.setId(oldCatalogue.getTaskId()); task.setId(oldCatalogue.getTaskId());
task.setName(catalogue.getName());
task.setAlias(catalogue.getName()); task.setAlias(catalogue.getName());
taskService.updateById(task); taskService.updateById(task);
this.updateById(catalogue); this.updateById(catalogue);
......
...@@ -27,6 +27,7 @@ import com.dlink.session.SessionConfig; ...@@ -27,6 +27,7 @@ import com.dlink.session.SessionConfig;
import com.dlink.session.SessionInfo; import com.dlink.session.SessionInfo;
import com.dlink.session.SessionPool; import com.dlink.session.SessionPool;
import com.dlink.utils.RunTimeUtil; import com.dlink.utils.RunTimeUtil;
import com.dlink.utils.UDFUtil;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
...@@ -89,6 +90,7 @@ public class StudioServiceImpl implements StudioService { ...@@ -89,6 +90,7 @@ public class StudioServiceImpl implements StudioService {
if(!config.isUseSession()) { if(!config.isUseSession()) {
config.setAddress(clusterService.buildEnvironmentAddress(config.isUseRemote(), studioExecuteDTO.getClusterId())); config.setAddress(clusterService.buildEnvironmentAddress(config.isUseRemote(), studioExecuteDTO.getClusterId()));
} }
initUDF(config,studioExecuteDTO.getStatement());
JobManager jobManager = JobManager.build(config); JobManager jobManager = JobManager.build(config);
JobResult jobResult = jobManager.executeSql(studioExecuteDTO.getStatement()); JobResult jobResult = jobManager.executeSql(studioExecuteDTO.getStatement());
RunTimeUtil.recovery(jobManager); RunTimeUtil.recovery(jobManager);
...@@ -152,6 +154,7 @@ public class StudioServiceImpl implements StudioService { ...@@ -152,6 +154,7 @@ public class StudioServiceImpl implements StudioService {
if(!config.isUseSession()) { if(!config.isUseSession()) {
config.setAddress(clusterService.buildEnvironmentAddress(config.isUseRemote(), studioExecuteDTO.getClusterId())); config.setAddress(clusterService.buildEnvironmentAddress(config.isUseRemote(), studioExecuteDTO.getClusterId()));
} }
initUDF(config,studioExecuteDTO.getStatement());
JobManager jobManager = JobManager.buildPlanMode(config); JobManager jobManager = JobManager.buildPlanMode(config);
return jobManager.explainSql(studioExecuteDTO.getStatement()).getSqlExplainResults(); return jobManager.explainSql(studioExecuteDTO.getStatement()).getSqlExplainResults();
} }
...@@ -317,4 +320,15 @@ public class StudioServiceImpl implements StudioService { ...@@ -317,4 +320,15 @@ public class StudioServiceImpl implements StudioService {
} }
return false; return false;
} }
private void initUDF(JobConfig config,String statement){
if(!GatewayType.LOCAL.equalsValue(config.getType())){
return;
}
List<String> udfClassNameList = JobManager.getUDFClassName(statement);
for(String item : udfClassNameList){
Task task = taskService.getUDFByClassName(item);
JobManager.initUDF(item,task.getStatement());
}
}
} }
...@@ -15,6 +15,7 @@ import com.dlink.job.JobResult; ...@@ -15,6 +15,7 @@ import com.dlink.job.JobResult;
import com.dlink.mapper.TaskMapper; import com.dlink.mapper.TaskMapper;
import com.dlink.model.*; import com.dlink.model.*;
import com.dlink.service.*; import com.dlink.service.*;
import com.dlink.utils.CustomStringJavaCompiler;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -100,6 +101,11 @@ public class TaskServiceImpl extends SuperServiceImpl<TaskMapper, Task> implemen ...@@ -100,6 +101,11 @@ public class TaskServiceImpl extends SuperServiceImpl<TaskMapper, Task> implemen
@Override @Override
public boolean saveOrUpdateTask(Task task) { public boolean saveOrUpdateTask(Task task) {
if(Asserts.isNotNullString(task.getDialect()) && Dialect.JAVA.equalsVal(task.getDialect())
&& Asserts.isNotNullString(task.getStatement()) ){
CustomStringJavaCompiler compiler = new CustomStringJavaCompiler(task.getStatement());
task.setSavePointPath(compiler.getFullClassName());
}
if (task.getId() != null) { if (task.getId() != null) {
this.updateById(task); this.updateById(task);
if (task.getStatement() != null) { if (task.getStatement() != null) {
...@@ -151,6 +157,14 @@ public class TaskServiceImpl extends SuperServiceImpl<TaskMapper, Task> implemen ...@@ -151,6 +157,14 @@ public class TaskServiceImpl extends SuperServiceImpl<TaskMapper, Task> implemen
} }
} }
@Override
public Task getUDFByClassName(String className) {
Task task = getOne(new QueryWrapper<Task>().eq("dialect", "Java").eq("enabled", 1).eq("save_point_path", className));
Assert.check(task);
task.setStatement(statementService.getById(task.getId()).getStatement());
return task;
}
private JobConfig buildJobConfig(Task task){ private JobConfig buildJobConfig(Task task){
boolean isJarTask = isJarTask(task); boolean isJarTask = isJarTask(task);
if(!isJarTask&&Asserts.isNotNull(task.getEnvId())){ if(!isJarTask&&Asserts.isNotNull(task.getEnvId())){
......
/*
* 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.flink.table.types.extraction;
import com.dlink.pool.ClassPool;
import org.apache.flink.annotation.Internal;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.catalog.DataTypeFactory;
import org.apache.flink.table.types.DataType;
import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassReader;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Label;
import org.apache.flink.shaded.asm7.org.objectweb.asm.MethodVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Opcodes;
import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getConstructorDescriptor;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getMethodDescriptor;
/** Utilities for performing reflection tasks. */
@Internal
public final class ExtractionUtils {
// --------------------------------------------------------------------------------------------
// Methods shared across packages
// --------------------------------------------------------------------------------------------
/** Collects methods of the given name. */
public static List<Method> collectMethods(Class<?> function, String methodName) {
return Arrays.stream(function.getMethods())
.filter(method -> method.getName().equals(methodName))
.sorted(Comparator.comparing(Method::toString)) // for deterministic order
.collect(Collectors.toList());
}
/**
* Checks whether a method/constructor can be called with the given argument classes. This
* includes type widening and vararg. {@code null} is a wildcard.
*
* <p>E.g., {@code (int.class, int.class)} matches {@code f(Object...), f(int, int), f(Integer,
* Object)} and so forth.
*/
public static boolean isInvokable(Executable executable, Class<?>... classes) {
final int m = executable.getModifiers();
if (!Modifier.isPublic(m)) {
return false;
}
final int paramCount = executable.getParameterCount();
final int classCount = classes.length;
// check for enough classes for each parameter
if ((!executable.isVarArgs() && classCount != paramCount)
|| (executable.isVarArgs() && classCount < paramCount - 1)) {
return false;
}
int currentClass = 0;
for (int currentParam = 0; currentParam < paramCount; currentParam++) {
final Class<?> param = executable.getParameterTypes()[currentParam];
// last parameter is a vararg that needs to consume remaining classes
if (currentParam == paramCount - 1 && executable.isVarArgs()) {
final Class<?> paramComponent =
executable.getParameterTypes()[currentParam].getComponentType();
// we have more than 1 classes left so the vararg needs to consume them all
if (classCount - currentClass > 1) {
while (currentClass < classCount
&& ExtractionUtils.isAssignable(
classes[currentClass], paramComponent, true)) {
currentClass++;
}
} else if (currentClass < classCount
&& (parameterMatches(classes[currentClass], param)
|| parameterMatches(classes[currentClass], paramComponent))) {
currentClass++;
}
}
// entire parameter matches
else if (parameterMatches(classes[currentClass], param)) {
currentClass++;
}
}
// check if all classes have been consumed
return currentClass == classCount;
}
private static boolean parameterMatches(Class<?> clz, Class<?> param) {
return clz == null || ExtractionUtils.isAssignable(clz, param, true);
}
/** Creates a method signature string like {@code int eval(Integer, String)}. */
public static String createMethodSignatureString(
String methodName, Class<?>[] parameters, @Nullable Class<?> returnType) {
final StringBuilder builder = new StringBuilder();
if (returnType != null) {
builder.append(returnType.getCanonicalName()).append(" ");
}
builder.append(methodName)
.append(
Stream.of(parameters)
.map(
parameter -> {
// in case we don't know the parameter at this location
// (i.e. for accumulators)
if (parameter == null) {
return "_";
} else {
return parameter.getCanonicalName();
}
})
.collect(Collectors.joining(", ", "(", ")")));
return builder.toString();
}
/**
* Validates the characteristics of a class for a {@link StructuredType} such as accessibility.
*/
public static void validateStructuredClass(Class<?> clazz) {
final int m = clazz.getModifiers();
if (Modifier.isAbstract(m)) {
throw extractionError("Class '%s' must not be abstract.", clazz.getName());
}
if (!Modifier.isPublic(m)) {
throw extractionError("Class '%s' is not public.", clazz.getName());
}
if (clazz.getEnclosingClass() != null
&& (clazz.getDeclaringClass() == null || !Modifier.isStatic(m))) {
throw extractionError(
"Class '%s' is a not a static, globally accessible class.", clazz.getName());
}
}
/**
* Returns the field of a structured type. The logic is as broad as possible to support both
* Java and Scala in different flavors.
*/
public static Field getStructuredField(Class<?> clazz, String fieldName) {
final String normalizedFieldName = fieldName.toUpperCase();
final List<Field> fields = collectStructuredFields(clazz);
for (Field field : fields) {
if (field.getName().toUpperCase().equals(normalizedFieldName)) {
return field;
}
}
throw extractionError(
"Could not to find a field named '%s' in class '%s' for structured type.",
fieldName, clazz.getName());
}
/**
* Checks for a field getter of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldGetter(Class<?> clazz, Field field) {
final String normalizedFieldName = field.getName().toUpperCase();
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// get<Name>()
// is<Name>()
// <Name>() for Scala
final String normalizedMethodName = method.getName().toUpperCase();
final boolean hasName =
normalizedMethodName.equals("GET" + normalizedFieldName)
|| normalizedMethodName.equals("IS" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName);
if (!hasName) {
continue;
}
// check return type:
// equal to field type
final Type returnType = method.getGenericReturnType();
final boolean hasReturnType = returnType.equals(field.getGenericType());
if (!hasReturnType) {
continue;
}
// check parameters:
// no parameters
final boolean hasNoParameters = method.getParameterCount() == 0;
if (!hasNoParameters) {
continue;
}
// matching getter found
return Optional.of(method);
}
// no getter found
return Optional.empty();
}
/**
* Checks for a field setters of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldSetter(Class<?> clazz, Field field) {
final String normalizedFieldName = field.getName().toUpperCase();
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// set<Name>(type)
// <Name>(type)
// <Name>_$eq(type) for Scala
final String normalizedMethodName = method.getName().toUpperCase();
final boolean hasName =
normalizedMethodName.equals("SET" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName + "_$EQ");
if (!hasName) {
continue;
}
// check return type:
// void or the declaring class
final Class<?> returnType = method.getReturnType();
final boolean hasReturnType = returnType == Void.TYPE || returnType == clazz;
if (!hasReturnType) {
continue;
}
// check parameters:
// one parameter that has the same (or primitive) type of the field
final boolean hasParameter =
method.getParameterCount() == 1
&& (method.getGenericParameterTypes()[0].equals(field.getGenericType())
|| primitiveToWrapper(method.getGenericParameterTypes()[0])
.equals(field.getGenericType()));
if (!hasParameter) {
continue;
}
// matching setter found
return Optional.of(method);
}
// no setter found
return Optional.empty();
}
/**
* Checks for an invokable constructor matching the given arguments.
*
* @see #isInvokable(Executable, Class[])
*/
public static boolean hasInvokableConstructor(Class<?> clazz, Class<?>... classes) {
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (isInvokable(constructor, classes)) {
return true;
}
}
return false;
}
/** Checks whether a field is directly readable without a getter. */
public static boolean isStructuredFieldDirectlyReadable(Field field) {
final int m = field.getModifiers();
// field is directly readable
return Modifier.isPublic(m);
}
/** Checks whether a field is directly writable without a setter or constructor. */
public static boolean isStructuredFieldDirectlyWritable(Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly writable
return Modifier.isPublic(m);
}
// --------------------------------------------------------------------------------------------
// Methods intended for this package
// --------------------------------------------------------------------------------------------
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(String message, Object... args) {
return extractionError(null, message, args);
}
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(Throwable cause, String message, Object... args) {
return new ValidationException(String.format(message, args), cause);
}
/**
* Collects the partially ordered type hierarchy (i.e. all involved super classes and super
* interfaces) of the given type.
*/
static List<Type> collectTypeHierarchy(Type type) {
Type currentType = type;
Class<?> currentClass = toClass(type);
final List<Type> typeHierarchy = new ArrayList<>();
while (currentClass != null) {
// collect type
typeHierarchy.add(currentType);
// collect super interfaces
for (Type genericInterface : currentClass.getGenericInterfaces()) {
final Class<?> interfaceClass = toClass(genericInterface);
if (interfaceClass != null) {
typeHierarchy.addAll(collectTypeHierarchy(genericInterface));
}
}
currentType = currentClass.getGenericSuperclass();
currentClass = toClass(currentType);
}
return typeHierarchy;
}
/** Converts a {@link Type} to {@link Class} if possible, {@code null} otherwise. */
static @Nullable Class<?> toClass(Type type) {
if (type instanceof Class) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
// this is always a class
return (Class<?>) ((ParameterizedType) type).getRawType();
}
// unsupported: generic arrays, type variables, wildcard types
return null;
}
/** Creates a raw data type. */
@SuppressWarnings({"unchecked", "rawtypes"})
static DataType createRawType(
DataTypeFactory typeFactory,
@Nullable Class<? extends TypeSerializer<?>> rawSerializer,
@Nullable Class<?> conversionClass) {
if (rawSerializer != null) {
return DataTypes.RAW(
(Class) createConversionClass(conversionClass),
instantiateRawSerializer(rawSerializer));
}
return typeFactory.createRawDataType(createConversionClass(conversionClass));
}
static Class<?> createConversionClass(@Nullable Class<?> conversionClass) {
if (conversionClass != null) {
return conversionClass;
}
return Object.class;
}
private static TypeSerializer<?> instantiateRawSerializer(
Class<? extends TypeSerializer<?>> rawSerializer) {
try {
return rawSerializer.newInstance();
} catch (Exception e) {
throw extractionError(
e,
"Cannot instantiate type serializer '%s' for RAW type. "
+ "Make sure the class is publicly accessible and has a default constructor.",
rawSerializer.getName());
}
}
/** Resolves a {@link TypeVariable} using the given type hierarchy if possible. */
static Type resolveVariable(List<Type> typeHierarchy, TypeVariable<?> variable) {
// iterate through hierarchy from top to bottom until type variable gets a non-variable
// assigned
for (int i = typeHierarchy.size() - 1; i >= 0; i--) {
final Type currentType = typeHierarchy.get(i);
if (currentType instanceof ParameterizedType) {
final Type resolvedType =
resolveVariableInParameterizedType(
variable, (ParameterizedType) currentType);
if (resolvedType instanceof TypeVariable) {
// follow type variables transitively
variable = (TypeVariable<?>) resolvedType;
} else if (resolvedType != null) {
return resolvedType;
}
}
}
// unresolved variable
return variable;
}
private static @Nullable Type resolveVariableInParameterizedType(
TypeVariable<?> variable, ParameterizedType currentType) {
final Class<?> currentRaw = (Class<?>) currentType.getRawType();
final TypeVariable<?>[] currentVariables = currentRaw.getTypeParameters();
// search for matching type variable
for (int paramPos = 0; paramPos < currentVariables.length; paramPos++) {
if (typeVariableEquals(variable, currentVariables[paramPos])) {
return currentType.getActualTypeArguments()[paramPos];
}
}
return null;
}
private static boolean typeVariableEquals(
TypeVariable<?> variable, TypeVariable<?> currentVariable) {
return currentVariable.getGenericDeclaration().equals(variable.getGenericDeclaration())
&& currentVariable.getName().equals(variable.getName());
}
/**
* Validates if a given type is not already contained in the type hierarchy of a structured
* type.
*
* <p>Otherwise this would lead to infinite data type extraction cycles.
*/
static void validateStructuredSelfReference(Type t, List<Type> typeHierarchy) {
final Class<?> clazz = toClass(t);
if (clazz != null
&& !clazz.isInterface()
&& clazz != Object.class
&& typeHierarchy.contains(t)) {
throw extractionError(
"Cyclic reference detected for class '%s'. Attributes of structured types must not "
+ "(transitively) reference the structured type itself.",
clazz.getName());
}
}
/** Returns the fields of a class for a {@link StructuredType}. */
static List<Field> collectStructuredFields(Class<?> clazz) {
final List<Field> fields = new ArrayList<>();
while (clazz != Object.class) {
final Field[] declaredFields = clazz.getDeclaredFields();
Stream.of(declaredFields)
.filter(
field -> {
final int m = field.getModifiers();
return !Modifier.isStatic(m) && !Modifier.isTransient(m);
})
.forEach(fields::add);
clazz = clazz.getSuperclass();
}
return fields;
}
/** Validates if a field is properly readable either directly or through a getter. */
static void validateStructuredFieldReadability(Class<?> clazz, Field field) {
// field is accessible
if (isStructuredFieldDirectlyReadable(field)) {
return;
}
// field needs a getter
if (!getStructuredFieldGetter(clazz, field).isPresent()) {
throw extractionError(
"Field '%s' of class '%s' is neither publicly accessible nor does it have "
+ "a corresponding getter method.",
field.getName(), clazz.getName());
}
}
/**
* Checks if a field is mutable or immutable. Returns {@code true} if the field is properly
* mutable. Returns {@code false} if it is properly immutable.
*/
static boolean isStructuredFieldMutable(Class<?> clazz, Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly mutable
if (Modifier.isPublic(m)) {
return true;
}
// field has setters by which it is mutable
if (getStructuredFieldSetter(clazz, field).isPresent()) {
return true;
}
throw extractionError(
"Field '%s' of class '%s' is mutable but is neither publicly accessible nor does it have "
+ "a corresponding setter method.",
field.getName(), clazz.getName());
}
/** Returns the boxed type of a primitive type. */
static Type primitiveToWrapper(Type type) {
if (type instanceof Class) {
return primitiveToWrapper((Class<?>) type);
}
return type;
}
/** Collects all methods that qualify as methods of a {@link StructuredType}. */
static List<Method> collectStructuredMethods(Class<?> clazz) {
final List<Method> methods = new ArrayList<>();
while (clazz != Object.class) {
final Method[] declaredMethods = clazz.getDeclaredMethods();
Stream.of(declaredMethods)
.filter(
field -> {
final int m = field.getModifiers();
return Modifier.isPublic(m)
&& !Modifier.isNative(m)
&& !Modifier.isAbstract(m);
})
.forEach(methods::add);
clazz = clazz.getSuperclass();
}
return methods;
}
/**
* Collects all annotations of the given type defined in the current class or superclasses.
* Duplicates are ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfClass(
Class<T> annotation, Class<?> annotatedClass) {
final List<Class<?>> classHierarchy = new ArrayList<>();
Class<?> currentClass = annotatedClass;
while (currentClass != null) {
classHierarchy.add(currentClass);
currentClass = currentClass.getSuperclass();
}
// convert to top down
Collections.reverse(classHierarchy);
return classHierarchy.stream()
.flatMap(c -> Stream.of(c.getAnnotationsByType(annotation)))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Collects all annotations of the given type defined in the given method. Duplicates are
* ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfMethod(
Class<T> annotation, Method annotatedMethod) {
return new LinkedHashSet<>(Arrays.asList(annotatedMethod.getAnnotationsByType(annotation)));
}
// --------------------------------------------------------------------------------------------
// Parameter Extraction Utilities
// --------------------------------------------------------------------------------------------
/** Result of the extraction in {@link #extractAssigningConstructor(Class, List)}. */
static class AssigningConstructor {
public final Constructor<?> constructor;
public final List<String> parameterNames;
private AssigningConstructor(Constructor<?> constructor, List<String> parameterNames) {
this.constructor = constructor;
this.parameterNames = parameterNames;
}
}
/**
* Checks whether the given constructor takes all of the given fields with matching (possibly
* primitive) type and name. An assigning constructor can define the order of fields.
*/
static @Nullable AssigningConstructor extractAssigningConstructor(
Class<?> clazz, List<Field> fields) {
AssigningConstructor foundConstructor = null;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
final boolean qualifyingConstructor =
Modifier.isPublic(constructor.getModifiers())
&& constructor.getParameterTypes().length == fields.size();
if (!qualifyingConstructor) {
continue;
}
final List<String> parameterNames =
extractConstructorParameterNames(constructor, fields);
if (parameterNames != null) {
if (foundConstructor != null) {
throw extractionError(
"Multiple constructors found that assign all fields for class '%s'.",
clazz.getName());
}
foundConstructor = new AssigningConstructor(constructor, parameterNames);
}
}
return foundConstructor;
}
/** Extracts the parameter names of a method if possible. */
static @Nullable List<String> extractMethodParameterNames(Method method) {
return extractExecutableNames(method);
}
/**
* Extracts ordered parameter names from a constructor that takes all of the given fields with
* matching (possibly primitive) type and name.
*/
private static @Nullable List<String> extractConstructorParameterNames(
Constructor<?> constructor, List<Field> fields) {
final Type[] parameterTypes = constructor.getGenericParameterTypes();
List<String> parameterNames = extractExecutableNames(constructor);
if (parameterNames == null) {
return null;
}
final Map<String, Type> fieldMap =
fields.stream().collect(Collectors.toMap(Field::getName, Field::getGenericType));
// check that all fields are represented in the parameters of the constructor
for (int i = 0; i < parameterNames.size(); i++) {
final String parameterName = parameterNames.get(i);
final Type fieldType = fieldMap.get(parameterName); // might be null
final Type parameterType = parameterTypes[i];
// we are tolerant here because frameworks such as Avro accept a boxed type even though
// the field is primitive
if (!primitiveToWrapper(parameterType).equals(primitiveToWrapper(fieldType))) {
return null;
}
}
return parameterNames;
}
private static @Nullable List<String> extractExecutableNames(Executable executable) {
final int offset;
if (!Modifier.isStatic(executable.getModifiers())) {
// remove "this" as first parameter
offset = 1;
} else {
offset = 0;
}
// by default parameter names are "arg0, arg1, arg2, ..." if compiler flag is not set
// so we need to extract them manually if possible
List<String> parameterNames =
Stream.of(executable.getParameters())
.map(Parameter::getName)
.collect(Collectors.toList());
if (parameterNames.stream().allMatch(n -> n.startsWith("arg"))) {
final ParameterExtractor extractor;
if (executable instanceof Constructor) {
extractor = new ParameterExtractor((Constructor<?>) executable);
} else {
extractor = new ParameterExtractor((Method) executable);
}
getClassReader(executable.getDeclaringClass()).accept(extractor, 0);
final List<String> extractedNames = extractor.getParameterNames();
if (extractedNames.size() == 0) {
return null;
}
// remove "this" and additional local variables
// select less names if class file has not the required information
parameterNames =
extractedNames.subList(
offset,
Math.min(
executable.getParameterCount() + offset,
extractedNames.size()));
}
if (parameterNames.size() != executable.getParameterCount()) {
return null;
}
return parameterNames;
}
private static ClassReader getClassReader(Class<?> cls) {
final String className = cls.getName().replaceFirst("^.*\\.", "") + ".class";
if(ClassPool.exist(cls.getName())){
return new ClassReader(ClassPool.get(cls.getName()).getClassByte());
}
try {
return new ClassReader(cls.getResourceAsStream(className));
} catch (IOException e) {
throw new IllegalStateException("Could not instantiate ClassReader.", e);
}
}
/**
* Extracts the parameter names and descriptors from a constructor or method. Assuming the
* existence of a local variable table.
*
* <p>For example:
*
* <pre>{@code
* public WC(java.lang.String arg0, long arg1) { // <init> //(Ljava/lang/String;J)V
* <localVar:index=0 , name=this , desc=Lorg/apache/flink/WC;, sig=null, start=L1, end=L2>
* <localVar:index=1 , name=word , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=frequency , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal2 , desc=J, sig=null, start=L1, end=L2>
* }
* }</pre>
*/
private static class ParameterExtractor extends ClassVisitor {
private static final int OPCODE = Opcodes.ASM7;
private final String methodDescriptor;
private final List<String> parameterNames = new ArrayList<>();
ParameterExtractor(Constructor<?> constructor) {
super(OPCODE);
methodDescriptor = getConstructorDescriptor(constructor);
}
ParameterExtractor(Method method) {
super(OPCODE);
methodDescriptor = getMethodDescriptor(method);
}
List<String> getParameterNames() {
return parameterNames;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
if (descriptor.equals(methodDescriptor)) {
return new MethodVisitor(OPCODE) {
@Override
public void visitLocalVariable(
String name,
String descriptor,
String signature,
Label start,
Label end,
int index) {
parameterNames.add(name);
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
// --------------------------------------------------------------------------------------------
// Class Assignment and Boxing
//
// copied from o.a.commons.lang3.ClassUtils (commons-lang3:3.3.2)
// --------------------------------------------------------------------------------------------
/**
* Checks if one {@code Class} can be assigned to a variable of another {@code Class}.
*
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this method takes into
* account widenings of primitive classes and {@code null}s.
*
* <p>Primitive widenings allow an int to be assigned to a long, float or double. This method
* returns the correct result for these cases.
*
* <p>{@code Null} may be assigned to any reference type. This method will return {@code true}
* if {@code null} is passed in and the toClass is non-primitive.
*
* <p>Specifically, this method tests whether the type represented by the specified {@code
* Class} parameter can be converted to the type represented by this {@code Class} object via an
* identity conversion widening primitive or widening reference conversion. See <em><a
* href="http://docs.oracle.com/javase/specs/">The Java Language Specification</a></em>,
* sections 5.1.1, 5.1.2 and 5.1.4 for details.
*
* @param cls the Class to check, may be null
* @param toClass the Class to try to assign into, returns false if null
* @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
* @return {@code true} if assignment possible
*/
public static boolean isAssignable(
Class<?> cls, final Class<?> toClass, final boolean autoboxing) {
if (toClass == null) {
return false;
}
// have to check for null, as isAssignableFrom doesn't
if (cls == null) {
return !toClass.isPrimitive();
}
// autoboxing:
if (autoboxing) {
if (cls.isPrimitive() && !toClass.isPrimitive()) {
cls = primitiveToWrapper(cls);
if (cls == null) {
return false;
}
}
if (toClass.isPrimitive() && !cls.isPrimitive()) {
cls = wrapperToPrimitive(cls);
if (cls == null) {
return false;
}
}
}
if (cls.equals(toClass)) {
return true;
}
if (cls.isPrimitive()) {
if (!toClass.isPrimitive()) {
return false;
}
if (Integer.TYPE.equals(cls)) {
return Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Long.TYPE.equals(cls)) {
return Float.TYPE.equals(toClass) || Double.TYPE.equals(toClass);
}
if (Boolean.TYPE.equals(cls)) {
return false;
}
if (Double.TYPE.equals(cls)) {
return false;
}
if (Float.TYPE.equals(cls)) {
return Double.TYPE.equals(toClass);
}
if (Character.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Short.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Byte.TYPE.equals(cls)) {
return Short.TYPE.equals(toClass)
|| Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
// should never get here
return false;
}
return toClass.isAssignableFrom(cls);
}
/** Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. */
private static final Map<Class<?>, Class<?>> primitiveWrapperMap = new HashMap<>();
static {
primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
primitiveWrapperMap.put(Byte.TYPE, Byte.class);
primitiveWrapperMap.put(Character.TYPE, Character.class);
primitiveWrapperMap.put(Short.TYPE, Short.class);
primitiveWrapperMap.put(Integer.TYPE, Integer.class);
primitiveWrapperMap.put(Long.TYPE, Long.class);
primitiveWrapperMap.put(Double.TYPE, Double.class);
primitiveWrapperMap.put(Float.TYPE, Float.class);
primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
}
/** Maps wrapper {@code Class}es to their corresponding primitive types. */
private static final Map<Class<?>, Class<?>> wrapperPrimitiveMap = new HashMap<>();
static {
for (final Class<?> primitiveClass : primitiveWrapperMap.keySet()) {
final Class<?> wrapperClass = primitiveWrapperMap.get(primitiveClass);
if (!primitiveClass.equals(wrapperClass)) {
wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
}
}
}
/**
* Converts the specified primitive Class object to its corresponding wrapper Class object.
*
* <p>NOTE: From v2.2, this method handles {@code Void.TYPE}, returning {@code Void.TYPE}.
*
* @param cls the class to convert, may be null
* @return the wrapper class for {@code cls} or {@code cls} if {@code cls} is not a primitive.
* {@code null} if null input.
* @since 2.1
*/
public static Class<?> primitiveToWrapper(final Class<?> cls) {
Class<?> convertedClass = cls;
if (cls != null && cls.isPrimitive()) {
convertedClass = primitiveWrapperMap.get(cls);
}
return convertedClass;
}
/**
* Converts the specified wrapper class to its corresponding primitive class.
*
* <p>This method is the counter part of {@code primitiveToWrapper()}. If the passed in class is
* a wrapper class for a primitive type, this primitive type will be returned (e.g. {@code
* Integer.TYPE} for {@code Integer.class}). For other classes, or if the parameter is
* <b>null</b>, the return value is <b>null</b>.
*
* @param cls the class to convert, may be <b>null</b>
* @return the corresponding primitive type if {@code cls} is a wrapper class, <b>null</b>
* otherwise
* @see #primitiveToWrapper(Class)
* @since 2.4
*/
public static Class<?> wrapperToPrimitive(final Class<?> cls) {
return wrapperPrimitiveMap.get(cls);
}
// --------------------------------------------------------------------------------------------
private ExtractionUtils() {
// no instantiation
}
}
/*
* 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.flink.table.types.extraction;
import com.dlink.pool.ClassPool;
import org.apache.flink.annotation.Internal;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.catalog.DataTypeFactory;
import org.apache.flink.table.types.DataType;
import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassReader;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Label;
import org.apache.flink.shaded.asm7.org.objectweb.asm.MethodVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Opcodes;
import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getConstructorDescriptor;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getMethodDescriptor;
/** Utilities for performing reflection tasks. */
@Internal
public final class ExtractionUtils {
// --------------------------------------------------------------------------------------------
// Methods shared across packages
// --------------------------------------------------------------------------------------------
/** Collects methods of the given name. */
public static List<Method> collectMethods(Class<?> function, String methodName) {
return Arrays.stream(function.getMethods())
.filter(method -> method.getName().equals(methodName))
.sorted(Comparator.comparing(Method::toString)) // for deterministic order
.collect(Collectors.toList());
}
/**
* Checks whether a method/constructor can be called with the given argument classes. This
* includes type widening and vararg. {@code null} is a wildcard.
*
* <p>E.g., {@code (int.class, int.class)} matches {@code f(Object...), f(int, int), f(Integer,
* Object)} and so forth.
*/
public static boolean isInvokable(Executable executable, Class<?>... classes) {
final int m = executable.getModifiers();
if (!Modifier.isPublic(m)) {
return false;
}
final int paramCount = executable.getParameterCount();
final int classCount = classes.length;
// check for enough classes for each parameter
if ((!executable.isVarArgs() && classCount != paramCount)
|| (executable.isVarArgs() && classCount < paramCount - 1)) {
return false;
}
int currentClass = 0;
for (int currentParam = 0; currentParam < paramCount; currentParam++) {
final Class<?> param = executable.getParameterTypes()[currentParam];
// last parameter is a vararg that needs to consume remaining classes
if (currentParam == paramCount - 1 && executable.isVarArgs()) {
final Class<?> paramComponent =
executable.getParameterTypes()[currentParam].getComponentType();
// we have more than 1 classes left so the vararg needs to consume them all
if (classCount - currentClass > 1) {
while (currentClass < classCount
&& ExtractionUtils.isAssignable(
classes[currentClass], paramComponent, true)) {
currentClass++;
}
} else if (currentClass < classCount
&& (parameterMatches(classes[currentClass], param)
|| parameterMatches(classes[currentClass], paramComponent))) {
currentClass++;
}
}
// entire parameter matches
else if (parameterMatches(classes[currentClass], param)) {
currentClass++;
}
}
// check if all classes have been consumed
return currentClass == classCount;
}
private static boolean parameterMatches(Class<?> clz, Class<?> param) {
return clz == null || ExtractionUtils.isAssignable(clz, param, true);
}
/** Creates a method signature string like {@code int eval(Integer, String)}. */
public static String createMethodSignatureString(
String methodName, Class<?>[] parameters, @Nullable Class<?> returnType) {
final StringBuilder builder = new StringBuilder();
if (returnType != null) {
builder.append(returnType.getCanonicalName()).append(" ");
}
builder.append(methodName)
.append(
Stream.of(parameters)
.map(
parameter -> {
// in case we don't know the parameter at this location
// (i.e. for accumulators)
if (parameter == null) {
return "_";
} else {
return parameter.getCanonicalName();
}
})
.collect(Collectors.joining(", ", "(", ")")));
return builder.toString();
}
/**
* Validates the characteristics of a class for a {@link StructuredType} such as accessibility.
*/
public static void validateStructuredClass(Class<?> clazz) {
final int m = clazz.getModifiers();
if (Modifier.isAbstract(m)) {
throw extractionError("Class '%s' must not be abstract.", clazz.getName());
}
if (!Modifier.isPublic(m)) {
throw extractionError("Class '%s' is not public.", clazz.getName());
}
if (clazz.getEnclosingClass() != null
&& (clazz.getDeclaringClass() == null || !Modifier.isStatic(m))) {
throw extractionError(
"Class '%s' is a not a static, globally accessible class.", clazz.getName());
}
}
/**
* Returns the field of a structured type. The logic is as broad as possible to support both
* Java and Scala in different flavors.
*/
public static Field getStructuredField(Class<?> clazz, String fieldName) {
final String normalizedFieldName = fieldName.toUpperCase();
final List<Field> fields = collectStructuredFields(clazz);
for (Field field : fields) {
if (field.getName().toUpperCase().equals(normalizedFieldName)) {
return field;
}
}
throw extractionError(
"Could not to find a field named '%s' in class '%s' for structured type.",
fieldName, clazz.getName());
}
/**
* Checks for a field getter of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldGetter(Class<?> clazz, Field field) {
final String normalizedFieldName = field.getName().toUpperCase();
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// get<Name>()
// is<Name>()
// <Name>() for Scala
final String normalizedMethodName = method.getName().toUpperCase();
final boolean hasName =
normalizedMethodName.equals("GET" + normalizedFieldName)
|| normalizedMethodName.equals("IS" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName);
if (!hasName) {
continue;
}
// check return type:
// equal to field type
final Type returnType = method.getGenericReturnType();
final boolean hasReturnType = returnType.equals(field.getGenericType());
if (!hasReturnType) {
continue;
}
// check parameters:
// no parameters
final boolean hasNoParameters = method.getParameterCount() == 0;
if (!hasNoParameters) {
continue;
}
// matching getter found
return Optional.of(method);
}
// no getter found
return Optional.empty();
}
/**
* Checks for a field setters of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldSetter(Class<?> clazz, Field field) {
final String normalizedFieldName = field.getName().toUpperCase();
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// set<Name>(type)
// <Name>(type)
// <Name>_$eq(type) for Scala
final String normalizedMethodName = method.getName().toUpperCase();
final boolean hasName =
normalizedMethodName.equals("SET" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName + "_$EQ");
if (!hasName) {
continue;
}
// check return type:
// void or the declaring class
final Class<?> returnType = method.getReturnType();
final boolean hasReturnType = returnType == Void.TYPE || returnType == clazz;
if (!hasReturnType) {
continue;
}
// check parameters:
// one parameter that has the same (or primitive) type of the field
final boolean hasParameter =
method.getParameterCount() == 1
&& (method.getGenericParameterTypes()[0].equals(field.getGenericType())
|| primitiveToWrapper(method.getGenericParameterTypes()[0])
.equals(field.getGenericType()));
if (!hasParameter) {
continue;
}
// matching setter found
return Optional.of(method);
}
// no setter found
return Optional.empty();
}
/**
* Checks for an invokable constructor matching the given arguments.
*
* @see #isInvokable(Executable, Class[])
*/
public static boolean hasInvokableConstructor(Class<?> clazz, Class<?>... classes) {
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (isInvokable(constructor, classes)) {
return true;
}
}
return false;
}
/** Checks whether a field is directly readable without a getter. */
public static boolean isStructuredFieldDirectlyReadable(Field field) {
final int m = field.getModifiers();
// field is directly readable
return Modifier.isPublic(m);
}
/** Checks whether a field is directly writable without a setter or constructor. */
public static boolean isStructuredFieldDirectlyWritable(Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly writable
return Modifier.isPublic(m);
}
// --------------------------------------------------------------------------------------------
// Methods intended for this package
// --------------------------------------------------------------------------------------------
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(String message, Object... args) {
return extractionError(null, message, args);
}
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(Throwable cause, String message, Object... args) {
return new ValidationException(String.format(message, args), cause);
}
/**
* Collects the partially ordered type hierarchy (i.e. all involved super classes and super
* interfaces) of the given type.
*/
static List<Type> collectTypeHierarchy(Type type) {
Type currentType = type;
Class<?> currentClass = toClass(type);
final List<Type> typeHierarchy = new ArrayList<>();
while (currentClass != null) {
// collect type
typeHierarchy.add(currentType);
// collect super interfaces
for (Type genericInterface : currentClass.getGenericInterfaces()) {
final Class<?> interfaceClass = toClass(genericInterface);
if (interfaceClass != null) {
typeHierarchy.addAll(collectTypeHierarchy(genericInterface));
}
}
currentType = currentClass.getGenericSuperclass();
currentClass = toClass(currentType);
}
return typeHierarchy;
}
/** Converts a {@link Type} to {@link Class} if possible, {@code null} otherwise. */
static @Nullable Class<?> toClass(Type type) {
if (type instanceof Class) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
// this is always a class
return (Class<?>) ((ParameterizedType) type).getRawType();
}
// unsupported: generic arrays, type variables, wildcard types
return null;
}
/** Creates a raw data type. */
@SuppressWarnings({"unchecked", "rawtypes"})
static DataType createRawType(
DataTypeFactory typeFactory,
@Nullable Class<? extends TypeSerializer<?>> rawSerializer,
@Nullable Class<?> conversionClass) {
if (rawSerializer != null) {
return DataTypes.RAW(
(Class) createConversionClass(conversionClass),
instantiateRawSerializer(rawSerializer));
}
return typeFactory.createRawDataType(createConversionClass(conversionClass));
}
static Class<?> createConversionClass(@Nullable Class<?> conversionClass) {
if (conversionClass != null) {
return conversionClass;
}
return Object.class;
}
private static TypeSerializer<?> instantiateRawSerializer(
Class<? extends TypeSerializer<?>> rawSerializer) {
try {
return rawSerializer.newInstance();
} catch (Exception e) {
throw extractionError(
e,
"Cannot instantiate type serializer '%s' for RAW type. "
+ "Make sure the class is publicly accessible and has a default constructor.",
rawSerializer.getName());
}
}
/** Resolves a {@link TypeVariable} using the given type hierarchy if possible. */
static Type resolveVariable(List<Type> typeHierarchy, TypeVariable<?> variable) {
// iterate through hierarchy from top to bottom until type variable gets a non-variable
// assigned
for (int i = typeHierarchy.size() - 1; i >= 0; i--) {
final Type currentType = typeHierarchy.get(i);
if (currentType instanceof ParameterizedType) {
final Type resolvedType =
resolveVariableInParameterizedType(
variable, (ParameterizedType) currentType);
if (resolvedType instanceof TypeVariable) {
// follow type variables transitively
variable = (TypeVariable<?>) resolvedType;
} else if (resolvedType != null) {
return resolvedType;
}
}
}
// unresolved variable
return variable;
}
private static @Nullable Type resolveVariableInParameterizedType(
TypeVariable<?> variable, ParameterizedType currentType) {
final Class<?> currentRaw = (Class<?>) currentType.getRawType();
final TypeVariable<?>[] currentVariables = currentRaw.getTypeParameters();
// search for matching type variable
for (int paramPos = 0; paramPos < currentVariables.length; paramPos++) {
if (typeVariableEquals(variable, currentVariables[paramPos])) {
return currentType.getActualTypeArguments()[paramPos];
}
}
return null;
}
private static boolean typeVariableEquals(
TypeVariable<?> variable, TypeVariable<?> currentVariable) {
return currentVariable.getGenericDeclaration().equals(variable.getGenericDeclaration())
&& currentVariable.getName().equals(variable.getName());
}
/**
* Validates if a given type is not already contained in the type hierarchy of a structured
* type.
*
* <p>Otherwise this would lead to infinite data type extraction cycles.
*/
static void validateStructuredSelfReference(Type t, List<Type> typeHierarchy) {
final Class<?> clazz = toClass(t);
if (clazz != null
&& !clazz.isInterface()
&& clazz != Object.class
&& typeHierarchy.contains(t)) {
throw extractionError(
"Cyclic reference detected for class '%s'. Attributes of structured types must not "
+ "(transitively) reference the structured type itself.",
clazz.getName());
}
}
/** Returns the fields of a class for a {@link StructuredType}. */
static List<Field> collectStructuredFields(Class<?> clazz) {
final List<Field> fields = new ArrayList<>();
while (clazz != Object.class) {
final Field[] declaredFields = clazz.getDeclaredFields();
Stream.of(declaredFields)
.filter(
field -> {
final int m = field.getModifiers();
return !Modifier.isStatic(m) && !Modifier.isTransient(m);
})
.forEach(fields::add);
clazz = clazz.getSuperclass();
}
return fields;
}
/** Validates if a field is properly readable either directly or through a getter. */
static void validateStructuredFieldReadability(Class<?> clazz, Field field) {
// field is accessible
if (isStructuredFieldDirectlyReadable(field)) {
return;
}
// field needs a getter
if (!getStructuredFieldGetter(clazz, field).isPresent()) {
throw extractionError(
"Field '%s' of class '%s' is neither publicly accessible nor does it have "
+ "a corresponding getter method.",
field.getName(), clazz.getName());
}
}
/**
* Checks if a field is mutable or immutable. Returns {@code true} if the field is properly
* mutable. Returns {@code false} if it is properly immutable.
*/
static boolean isStructuredFieldMutable(Class<?> clazz, Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly mutable
if (Modifier.isPublic(m)) {
return true;
}
// field has setters by which it is mutable
if (getStructuredFieldSetter(clazz, field).isPresent()) {
return true;
}
throw extractionError(
"Field '%s' of class '%s' is mutable but is neither publicly accessible nor does it have "
+ "a corresponding setter method.",
field.getName(), clazz.getName());
}
/** Returns the boxed type of a primitive type. */
static Type primitiveToWrapper(Type type) {
if (type instanceof Class) {
return primitiveToWrapper((Class<?>) type);
}
return type;
}
/** Collects all methods that qualify as methods of a {@link StructuredType}. */
static List<Method> collectStructuredMethods(Class<?> clazz) {
final List<Method> methods = new ArrayList<>();
while (clazz != Object.class) {
final Method[] declaredMethods = clazz.getDeclaredMethods();
Stream.of(declaredMethods)
.filter(
field -> {
final int m = field.getModifiers();
return Modifier.isPublic(m)
&& !Modifier.isNative(m)
&& !Modifier.isAbstract(m);
})
.forEach(methods::add);
clazz = clazz.getSuperclass();
}
return methods;
}
/**
* Collects all annotations of the given type defined in the current class or superclasses.
* Duplicates are ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfClass(
Class<T> annotation, Class<?> annotatedClass) {
final List<Class<?>> classHierarchy = new ArrayList<>();
Class<?> currentClass = annotatedClass;
while (currentClass != null) {
classHierarchy.add(currentClass);
currentClass = currentClass.getSuperclass();
}
// convert to top down
Collections.reverse(classHierarchy);
return classHierarchy.stream()
.flatMap(c -> Stream.of(c.getAnnotationsByType(annotation)))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Collects all annotations of the given type defined in the given method. Duplicates are
* ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfMethod(
Class<T> annotation, Method annotatedMethod) {
return new LinkedHashSet<>(Arrays.asList(annotatedMethod.getAnnotationsByType(annotation)));
}
// --------------------------------------------------------------------------------------------
// Parameter Extraction Utilities
// --------------------------------------------------------------------------------------------
/** Result of the extraction in {@link #extractAssigningConstructor(Class, List)}. */
static class AssigningConstructor {
public final Constructor<?> constructor;
public final List<String> parameterNames;
private AssigningConstructor(Constructor<?> constructor, List<String> parameterNames) {
this.constructor = constructor;
this.parameterNames = parameterNames;
}
}
/**
* Checks whether the given constructor takes all of the given fields with matching (possibly
* primitive) type and name. An assigning constructor can define the order of fields.
*/
static @Nullable AssigningConstructor extractAssigningConstructor(
Class<?> clazz, List<Field> fields) {
AssigningConstructor foundConstructor = null;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
final boolean qualifyingConstructor =
Modifier.isPublic(constructor.getModifiers())
&& constructor.getParameterTypes().length == fields.size();
if (!qualifyingConstructor) {
continue;
}
final List<String> parameterNames =
extractConstructorParameterNames(constructor, fields);
if (parameterNames != null) {
if (foundConstructor != null) {
throw extractionError(
"Multiple constructors found that assign all fields for class '%s'.",
clazz.getName());
}
foundConstructor = new AssigningConstructor(constructor, parameterNames);
}
}
return foundConstructor;
}
/** Extracts the parameter names of a method if possible. */
static @Nullable List<String> extractMethodParameterNames(Method method) {
return extractExecutableNames(method);
}
/**
* Extracts ordered parameter names from a constructor that takes all of the given fields with
* matching (possibly primitive) type and name.
*/
private static @Nullable List<String> extractConstructorParameterNames(
Constructor<?> constructor, List<Field> fields) {
final Type[] parameterTypes = constructor.getGenericParameterTypes();
List<String> parameterNames = extractExecutableNames(constructor);
if (parameterNames == null) {
return null;
}
final Map<String, Type> fieldMap =
fields.stream().collect(Collectors.toMap(Field::getName, Field::getGenericType));
// check that all fields are represented in the parameters of the constructor
for (int i = 0; i < parameterNames.size(); i++) {
final String parameterName = parameterNames.get(i);
final Type fieldType = fieldMap.get(parameterName); // might be null
final Type parameterType = parameterTypes[i];
// we are tolerant here because frameworks such as Avro accept a boxed type even though
// the field is primitive
if (!primitiveToWrapper(parameterType).equals(primitiveToWrapper(fieldType))) {
return null;
}
}
return parameterNames;
}
private static @Nullable List<String> extractExecutableNames(Executable executable) {
final int offset;
if (!Modifier.isStatic(executable.getModifiers())) {
// remove "this" as first parameter
offset = 1;
} else {
offset = 0;
}
// by default parameter names are "arg0, arg1, arg2, ..." if compiler flag is not set
// so we need to extract them manually if possible
List<String> parameterNames =
Stream.of(executable.getParameters())
.map(Parameter::getName)
.collect(Collectors.toList());
if (parameterNames.stream().allMatch(n -> n.startsWith("arg"))) {
final ParameterExtractor extractor;
if (executable instanceof Constructor) {
extractor = new ParameterExtractor((Constructor<?>) executable);
} else {
extractor = new ParameterExtractor((Method) executable);
}
getClassReader(executable.getDeclaringClass()).accept(extractor, 0);
final List<String> extractedNames = extractor.getParameterNames();
if (extractedNames.size() == 0) {
return null;
}
// remove "this" and additional local variables
// select less names if class file has not the required information
parameterNames =
extractedNames.subList(
offset,
Math.min(
executable.getParameterCount() + offset,
extractedNames.size()));
}
if (parameterNames.size() != executable.getParameterCount()) {
return null;
}
return parameterNames;
}
private static ClassReader getClassReader(Class<?> cls) {
final String className = cls.getName().replaceFirst("^.*\\.", "") + ".class";
if(ClassPool.exist(cls.getName())){
return new ClassReader(ClassPool.get(cls.getName()).getClassByte());
}
try {
return new ClassReader(cls.getResourceAsStream(className));
} catch (IOException e) {
throw new IllegalStateException("Could not instantiate ClassReader.", e);
}
}
/**
* Extracts the parameter names and descriptors from a constructor or method. Assuming the
* existence of a local variable table.
*
* <p>For example:
*
* <pre>{@code
* public WC(java.lang.String arg0, long arg1) { // <init> //(Ljava/lang/String;J)V
* <localVar:index=0 , name=this , desc=Lorg/apache/flink/WC;, sig=null, start=L1, end=L2>
* <localVar:index=1 , name=word , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=frequency , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal2 , desc=J, sig=null, start=L1, end=L2>
* }
* }</pre>
*/
private static class ParameterExtractor extends ClassVisitor {
private static final int OPCODE = Opcodes.ASM7;
private final String methodDescriptor;
private final List<String> parameterNames = new ArrayList<>();
ParameterExtractor(Constructor<?> constructor) {
super(OPCODE);
methodDescriptor = getConstructorDescriptor(constructor);
}
ParameterExtractor(Method method) {
super(OPCODE);
methodDescriptor = getMethodDescriptor(method);
}
List<String> getParameterNames() {
return parameterNames;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
if (descriptor.equals(methodDescriptor)) {
return new MethodVisitor(OPCODE) {
@Override
public void visitLocalVariable(
String name,
String descriptor,
String signature,
Label start,
Label end,
int index) {
parameterNames.add(name);
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
// --------------------------------------------------------------------------------------------
// Class Assignment and Boxing
//
// copied from o.a.commons.lang3.ClassUtils (commons-lang3:3.3.2)
// --------------------------------------------------------------------------------------------
/**
* Checks if one {@code Class} can be assigned to a variable of another {@code Class}.
*
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this method takes into
* account widenings of primitive classes and {@code null}s.
*
* <p>Primitive widenings allow an int to be assigned to a long, float or double. This method
* returns the correct result for these cases.
*
* <p>{@code Null} may be assigned to any reference type. This method will return {@code true}
* if {@code null} is passed in and the toClass is non-primitive.
*
* <p>Specifically, this method tests whether the type represented by the specified {@code
* Class} parameter can be converted to the type represented by this {@code Class} object via an
* identity conversion widening primitive or widening reference conversion. See <em><a
* href="http://docs.oracle.com/javase/specs/">The Java Language Specification</a></em>,
* sections 5.1.1, 5.1.2 and 5.1.4 for details.
*
* @param cls the Class to check, may be null
* @param toClass the Class to try to assign into, returns false if null
* @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
* @return {@code true} if assignment possible
*/
public static boolean isAssignable(
Class<?> cls, final Class<?> toClass, final boolean autoboxing) {
if (toClass == null) {
return false;
}
// have to check for null, as isAssignableFrom doesn't
if (cls == null) {
return !toClass.isPrimitive();
}
// autoboxing:
if (autoboxing) {
if (cls.isPrimitive() && !toClass.isPrimitive()) {
cls = primitiveToWrapper(cls);
if (cls == null) {
return false;
}
}
if (toClass.isPrimitive() && !cls.isPrimitive()) {
cls = wrapperToPrimitive(cls);
if (cls == null) {
return false;
}
}
}
if (cls.equals(toClass)) {
return true;
}
if (cls.isPrimitive()) {
if (!toClass.isPrimitive()) {
return false;
}
if (Integer.TYPE.equals(cls)) {
return Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Long.TYPE.equals(cls)) {
return Float.TYPE.equals(toClass) || Double.TYPE.equals(toClass);
}
if (Boolean.TYPE.equals(cls)) {
return false;
}
if (Double.TYPE.equals(cls)) {
return false;
}
if (Float.TYPE.equals(cls)) {
return Double.TYPE.equals(toClass);
}
if (Character.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Short.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Byte.TYPE.equals(cls)) {
return Short.TYPE.equals(toClass)
|| Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
// should never get here
return false;
}
return toClass.isAssignableFrom(cls);
}
/** Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. */
private static final Map<Class<?>, Class<?>> primitiveWrapperMap = new HashMap<>();
static {
primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
primitiveWrapperMap.put(Byte.TYPE, Byte.class);
primitiveWrapperMap.put(Character.TYPE, Character.class);
primitiveWrapperMap.put(Short.TYPE, Short.class);
primitiveWrapperMap.put(Integer.TYPE, Integer.class);
primitiveWrapperMap.put(Long.TYPE, Long.class);
primitiveWrapperMap.put(Double.TYPE, Double.class);
primitiveWrapperMap.put(Float.TYPE, Float.class);
primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
}
/** Maps wrapper {@code Class}es to their corresponding primitive types. */
private static final Map<Class<?>, Class<?>> wrapperPrimitiveMap = new HashMap<>();
static {
for (final Class<?> primitiveClass : primitiveWrapperMap.keySet()) {
final Class<?> wrapperClass = primitiveWrapperMap.get(primitiveClass);
if (!primitiveClass.equals(wrapperClass)) {
wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
}
}
}
/**
* Converts the specified primitive Class object to its corresponding wrapper Class object.
*
* <p>NOTE: From v2.2, this method handles {@code Void.TYPE}, returning {@code Void.TYPE}.
*
* @param cls the class to convert, may be null
* @return the wrapper class for {@code cls} or {@code cls} if {@code cls} is not a primitive.
* {@code null} if null input.
* @since 2.1
*/
public static Class<?> primitiveToWrapper(final Class<?> cls) {
Class<?> convertedClass = cls;
if (cls != null && cls.isPrimitive()) {
convertedClass = primitiveWrapperMap.get(cls);
}
return convertedClass;
}
/**
* Converts the specified wrapper class to its corresponding primitive class.
*
* <p>This method is the counter part of {@code primitiveToWrapper()}. If the passed in class is
* a wrapper class for a primitive type, this primitive type will be returned (e.g. {@code
* Integer.TYPE} for {@code Integer.class}). For other classes, or if the parameter is
* <b>null</b>, the return value is <b>null</b>.
*
* @param cls the class to convert, may be <b>null</b>
* @return the corresponding primitive type if {@code cls} is a wrapper class, <b>null</b>
* otherwise
* @see #primitiveToWrapper(Class)
* @since 2.4
*/
public static Class<?> wrapperToPrimitive(final Class<?> cls) {
return wrapperPrimitiveMap.get(cls);
}
// --------------------------------------------------------------------------------------------
private ExtractionUtils() {
// no instantiation
}
}
/*
* 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.flink.table.types.extraction;
import com.dlink.pool.ClassPool;
import org.apache.flink.annotation.Internal;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.catalog.DataTypeFactory;
import org.apache.flink.table.types.DataType;
import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassReader;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Label;
import org.apache.flink.shaded.asm7.org.objectweb.asm.MethodVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Opcodes;
import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getConstructorDescriptor;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getMethodDescriptor;
/** Utilities for performing reflection tasks. */
@Internal
public final class ExtractionUtils {
// --------------------------------------------------------------------------------------------
// Methods shared across packages
// --------------------------------------------------------------------------------------------
/** Collects methods of the given name. */
public static List<Method> collectMethods(Class<?> function, String methodName) {
return Arrays.stream(function.getMethods())
.filter(method -> method.getName().equals(methodName))
.sorted(Comparator.comparing(Method::toString)) // for deterministic order
.collect(Collectors.toList());
}
/**
* Checks whether a method/constructor can be called with the given argument classes. This
* includes type widening and vararg. {@code null} is a wildcard.
*
* <p>E.g., {@code (int.class, int.class)} matches {@code f(Object...), f(int, int), f(Integer,
* Object)} and so forth.
*/
public static boolean isInvokable(Executable executable, Class<?>... classes) {
final int m = executable.getModifiers();
if (!Modifier.isPublic(m)) {
return false;
}
final int paramCount = executable.getParameterCount();
final int classCount = classes.length;
// check for enough classes for each parameter
if ((!executable.isVarArgs() && classCount != paramCount)
|| (executable.isVarArgs() && classCount < paramCount - 1)) {
return false;
}
int currentClass = 0;
for (int currentParam = 0; currentParam < paramCount; currentParam++) {
final Class<?> param = executable.getParameterTypes()[currentParam];
// last parameter is a vararg that needs to consume remaining classes
if (currentParam == paramCount - 1 && executable.isVarArgs()) {
final Class<?> paramComponent =
executable.getParameterTypes()[currentParam].getComponentType();
// we have more than 1 classes left so the vararg needs to consume them all
if (classCount - currentClass > 1) {
while (currentClass < classCount
&& ExtractionUtils.isAssignable(
classes[currentClass], paramComponent, true)) {
currentClass++;
}
} else if (currentClass < classCount
&& (parameterMatches(classes[currentClass], param)
|| parameterMatches(classes[currentClass], paramComponent))) {
currentClass++;
}
}
// entire parameter matches
else if (parameterMatches(classes[currentClass], param)) {
currentClass++;
}
}
// check if all classes have been consumed
return currentClass == classCount;
}
private static boolean parameterMatches(Class<?> clz, Class<?> param) {
return clz == null || ExtractionUtils.isAssignable(clz, param, true);
}
/** Creates a method signature string like {@code int eval(Integer, String)}. */
public static String createMethodSignatureString(
String methodName, Class<?>[] parameters, @Nullable Class<?> returnType) {
final StringBuilder builder = new StringBuilder();
if (returnType != null) {
builder.append(returnType.getCanonicalName()).append(" ");
}
builder.append(methodName)
.append(
Stream.of(parameters)
.map(
parameter -> {
// in case we don't know the parameter at this location
// (i.e. for accumulators)
if (parameter == null) {
return "_";
} else {
return parameter.getCanonicalName();
}
})
.collect(Collectors.joining(", ", "(", ")")));
return builder.toString();
}
/**
* Validates the characteristics of a class for a {@link StructuredType} such as accessibility.
*/
public static void validateStructuredClass(Class<?> clazz) {
final int m = clazz.getModifiers();
if (Modifier.isAbstract(m)) {
throw extractionError("Class '%s' must not be abstract.", clazz.getName());
}
if (!Modifier.isPublic(m)) {
throw extractionError("Class '%s' is not public.", clazz.getName());
}
if (clazz.getEnclosingClass() != null
&& (clazz.getDeclaringClass() == null || !Modifier.isStatic(m))) {
throw extractionError(
"Class '%s' is a not a static, globally accessible class.", clazz.getName());
}
}
/**
* Returns the field of a structured type. The logic is as broad as possible to support both
* Java and Scala in different flavors.
*/
public static Field getStructuredField(Class<?> clazz, String fieldName) {
final String normalizedFieldName = fieldName.toUpperCase();
final List<Field> fields = collectStructuredFields(clazz);
for (Field field : fields) {
if (field.getName().toUpperCase().equals(normalizedFieldName)) {
return field;
}
}
throw extractionError(
"Could not find a field named '%s' in class '%s' for structured type.",
fieldName, clazz.getName());
}
/**
* Checks for a field getter of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldGetter(Class<?> clazz, Field field) {
final String normalizedFieldName = normalizeAccessorName(field.getName());
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// get<Name>()
// is<Name>()
// <Name>() for Scala
final String normalizedMethodName = normalizeAccessorName(method.getName());
final boolean hasName =
normalizedMethodName.equals("GET" + normalizedFieldName)
|| normalizedMethodName.equals("IS" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName);
if (!hasName) {
continue;
}
// check return type:
// equal to field type
final Type returnType = method.getGenericReturnType();
final boolean hasReturnType = returnType.equals(field.getGenericType());
if (!hasReturnType) {
continue;
}
// check parameters:
// no parameters
final boolean hasNoParameters = method.getParameterCount() == 0;
if (!hasNoParameters) {
continue;
}
// matching getter found
return Optional.of(method);
}
// no getter found
return Optional.empty();
}
/**
* Checks for a field setters of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldSetter(Class<?> clazz, Field field) {
final String normalizedFieldName = normalizeAccessorName(field.getName());
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// set<Name>(type)
// <Name>(type)
// <Name>_$eq(type) for Scala
final String normalizedMethodName = normalizeAccessorName(method.getName());
final boolean hasName =
normalizedMethodName.equals("SET" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName + "$EQ");
if (!hasName) {
continue;
}
// check return type:
// void or the declaring class
final Class<?> returnType = method.getReturnType();
final boolean hasReturnType = returnType == Void.TYPE || returnType == clazz;
if (!hasReturnType) {
continue;
}
// check parameters:
// one parameter that has the same (or primitive) type of the field
final boolean hasParameter =
method.getParameterCount() == 1
&& (method.getGenericParameterTypes()[0].equals(field.getGenericType())
|| primitiveToWrapper(method.getGenericParameterTypes()[0])
.equals(field.getGenericType()));
if (!hasParameter) {
continue;
}
// matching setter found
return Optional.of(method);
}
// no setter found
return Optional.empty();
}
private static String normalizeAccessorName(String name) {
return name.toUpperCase().replaceAll(Pattern.quote("_"), "");
}
/**
* Checks for an invokable constructor matching the given arguments.
*
* @see #isInvokable(Executable, Class[])
*/
public static boolean hasInvokableConstructor(Class<?> clazz, Class<?>... classes) {
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (isInvokable(constructor, classes)) {
return true;
}
}
return false;
}
/** Checks whether a field is directly readable without a getter. */
public static boolean isStructuredFieldDirectlyReadable(Field field) {
final int m = field.getModifiers();
// field is directly readable
return Modifier.isPublic(m);
}
/** Checks whether a field is directly writable without a setter or constructor. */
public static boolean isStructuredFieldDirectlyWritable(Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly writable
return Modifier.isPublic(m);
}
/**
* A minimal version to extract a generic parameter from a given class.
*
* <p>This method should only be used for very specific use cases, in most cases {@link
* DataTypeExtractor#extractFromGeneric(DataTypeFactory, Class, int, Type)} should be more
* appropriate.
*/
public static Optional<Class<?>> extractSimpleGeneric(
Class<?> baseClass, Class<?> clazz, int pos) {
try {
if (clazz.getSuperclass() != baseClass) {
return Optional.empty();
}
final Type t =
((ParameterizedType) clazz.getGenericSuperclass())
.getActualTypeArguments()[pos];
return Optional.ofNullable(toClass(t));
} catch (Exception unused) {
return Optional.empty();
}
}
// --------------------------------------------------------------------------------------------
// Methods intended for this package
// --------------------------------------------------------------------------------------------
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(String message, Object... args) {
return extractionError(null, message, args);
}
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(Throwable cause, String message, Object... args) {
return new ValidationException(String.format(message, args), cause);
}
/**
* Collects the partially ordered type hierarchy (i.e. all involved super classes and super
* interfaces) of the given type.
*/
static List<Type> collectTypeHierarchy(Type type) {
Type currentType = type;
Class<?> currentClass = toClass(type);
final List<Type> typeHierarchy = new ArrayList<>();
while (currentClass != null) {
// collect type
typeHierarchy.add(currentType);
// collect super interfaces
for (Type genericInterface : currentClass.getGenericInterfaces()) {
final Class<?> interfaceClass = toClass(genericInterface);
if (interfaceClass != null) {
typeHierarchy.addAll(collectTypeHierarchy(genericInterface));
}
}
currentType = currentClass.getGenericSuperclass();
currentClass = toClass(currentType);
}
return typeHierarchy;
}
/** Converts a {@link Type} to {@link Class} if possible, {@code null} otherwise. */
static @Nullable Class<?> toClass(Type type) {
if (type instanceof Class) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
// this is always a class
return (Class<?>) ((ParameterizedType) type).getRawType();
}
// unsupported: generic arrays, type variables, wildcard types
return null;
}
/** Creates a raw data type. */
@SuppressWarnings({"unchecked", "rawtypes"})
static DataType createRawType(
DataTypeFactory typeFactory,
@Nullable Class<? extends TypeSerializer<?>> rawSerializer,
@Nullable Class<?> conversionClass) {
if (rawSerializer != null) {
return DataTypes.RAW(
(Class) createConversionClass(conversionClass),
instantiateRawSerializer(rawSerializer));
}
return typeFactory.createRawDataType(createConversionClass(conversionClass));
}
static Class<?> createConversionClass(@Nullable Class<?> conversionClass) {
if (conversionClass != null) {
return conversionClass;
}
return Object.class;
}
private static TypeSerializer<?> instantiateRawSerializer(
Class<? extends TypeSerializer<?>> rawSerializer) {
try {
return rawSerializer.newInstance();
} catch (Exception e) {
throw extractionError(
e,
"Cannot instantiate type serializer '%s' for RAW type. "
+ "Make sure the class is publicly accessible and has a default constructor.",
rawSerializer.getName());
}
}
/** Resolves a {@link TypeVariable} using the given type hierarchy if possible. */
static Type resolveVariable(List<Type> typeHierarchy, TypeVariable<?> variable) {
// iterate through hierarchy from top to bottom until type variable gets a non-variable
// assigned
for (int i = typeHierarchy.size() - 1; i >= 0; i--) {
final Type currentType = typeHierarchy.get(i);
if (currentType instanceof ParameterizedType) {
final Type resolvedType =
resolveVariableInParameterizedType(
variable, (ParameterizedType) currentType);
if (resolvedType instanceof TypeVariable) {
// follow type variables transitively
variable = (TypeVariable<?>) resolvedType;
} else if (resolvedType != null) {
return resolvedType;
}
}
}
// unresolved variable
return variable;
}
private static @Nullable Type resolveVariableInParameterizedType(
TypeVariable<?> variable, ParameterizedType currentType) {
final Class<?> currentRaw = (Class<?>) currentType.getRawType();
final TypeVariable<?>[] currentVariables = currentRaw.getTypeParameters();
// search for matching type variable
for (int paramPos = 0; paramPos < currentVariables.length; paramPos++) {
if (typeVariableEquals(variable, currentVariables[paramPos])) {
return currentType.getActualTypeArguments()[paramPos];
}
}
return null;
}
private static boolean typeVariableEquals(
TypeVariable<?> variable, TypeVariable<?> currentVariable) {
return currentVariable.getGenericDeclaration().equals(variable.getGenericDeclaration())
&& currentVariable.getName().equals(variable.getName());
}
/**
* Validates if a given type is not already contained in the type hierarchy of a structured
* type.
*
* <p>Otherwise this would lead to infinite data type extraction cycles.
*/
static void validateStructuredSelfReference(Type t, List<Type> typeHierarchy) {
final Class<?> clazz = toClass(t);
if (clazz != null
&& !clazz.isInterface()
&& clazz != Object.class
&& typeHierarchy.contains(t)) {
throw extractionError(
"Cyclic reference detected for class '%s'. Attributes of structured types must not "
+ "(transitively) reference the structured type itself.",
clazz.getName());
}
}
/** Returns the fields of a class for a {@link StructuredType}. */
static List<Field> collectStructuredFields(Class<?> clazz) {
final List<Field> fields = new ArrayList<>();
while (clazz != Object.class) {
final Field[] declaredFields = clazz.getDeclaredFields();
Stream.of(declaredFields)
.filter(
field -> {
final int m = field.getModifiers();
return !Modifier.isStatic(m) && !Modifier.isTransient(m);
})
.forEach(fields::add);
clazz = clazz.getSuperclass();
}
return fields;
}
/** Validates if a field is properly readable either directly or through a getter. */
static void validateStructuredFieldReadability(Class<?> clazz, Field field) {
// field is accessible
if (isStructuredFieldDirectlyReadable(field)) {
return;
}
// field needs a getter
if (!getStructuredFieldGetter(clazz, field).isPresent()) {
throw extractionError(
"Field '%s' of class '%s' is neither publicly accessible nor does it have "
+ "a corresponding getter method.",
field.getName(), clazz.getName());
}
}
/**
* Checks if a field is mutable or immutable. Returns {@code true} if the field is properly
* mutable. Returns {@code false} if it is properly immutable.
*/
static boolean isStructuredFieldMutable(Class<?> clazz, Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly mutable
if (Modifier.isPublic(m)) {
return true;
}
// field has setters by which it is mutable
if (getStructuredFieldSetter(clazz, field).isPresent()) {
return true;
}
throw extractionError(
"Field '%s' of class '%s' is mutable but is neither publicly accessible nor does it have "
+ "a corresponding setter method.",
field.getName(), clazz.getName());
}
/** Returns the boxed type of a primitive type. */
static Type primitiveToWrapper(Type type) {
if (type instanceof Class) {
return primitiveToWrapper((Class<?>) type);
}
return type;
}
/** Collects all methods that qualify as methods of a {@link StructuredType}. */
static List<Method> collectStructuredMethods(Class<?> clazz) {
final List<Method> methods = new ArrayList<>();
while (clazz != Object.class) {
final Method[] declaredMethods = clazz.getDeclaredMethods();
Stream.of(declaredMethods)
.filter(
field -> {
final int m = field.getModifiers();
return Modifier.isPublic(m)
&& !Modifier.isNative(m)
&& !Modifier.isAbstract(m);
})
.forEach(methods::add);
clazz = clazz.getSuperclass();
}
return methods;
}
/**
* Collects all annotations of the given type defined in the current class or superclasses.
* Duplicates are ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfClass(
Class<T> annotation, Class<?> annotatedClass) {
final List<Class<?>> classHierarchy = new ArrayList<>();
Class<?> currentClass = annotatedClass;
while (currentClass != null) {
classHierarchy.add(currentClass);
currentClass = currentClass.getSuperclass();
}
// convert to top down
Collections.reverse(classHierarchy);
return classHierarchy.stream()
.flatMap(c -> Stream.of(c.getAnnotationsByType(annotation)))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Collects all annotations of the given type defined in the given method. Duplicates are
* ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfMethod(
Class<T> annotation, Method annotatedMethod) {
return new LinkedHashSet<>(Arrays.asList(annotatedMethod.getAnnotationsByType(annotation)));
}
// --------------------------------------------------------------------------------------------
// Parameter Extraction Utilities
// --------------------------------------------------------------------------------------------
/** Result of the extraction in {@link #extractAssigningConstructor(Class, List)}. */
public static class AssigningConstructor {
public final Constructor<?> constructor;
public final List<String> parameterNames;
private AssigningConstructor(Constructor<?> constructor, List<String> parameterNames) {
this.constructor = constructor;
this.parameterNames = parameterNames;
}
}
/**
* Checks whether the given constructor takes all of the given fields with matching (possibly
* primitive) type and name. An assigning constructor can define the order of fields.
*/
public static @Nullable AssigningConstructor extractAssigningConstructor(
Class<?> clazz, List<Field> fields) {
AssigningConstructor foundConstructor = null;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
final boolean qualifyingConstructor =
Modifier.isPublic(constructor.getModifiers())
&& constructor.getParameterTypes().length == fields.size();
if (!qualifyingConstructor) {
continue;
}
final List<String> parameterNames =
extractConstructorParameterNames(constructor, fields);
if (parameterNames != null) {
if (foundConstructor != null) {
throw extractionError(
"Multiple constructors found that assign all fields for class '%s'.",
clazz.getName());
}
foundConstructor = new AssigningConstructor(constructor, parameterNames);
}
}
return foundConstructor;
}
/** Extracts the parameter names of a method if possible. */
static @Nullable List<String> extractMethodParameterNames(Method method) {
return extractExecutableNames(method);
}
/**
* Extracts ordered parameter names from a constructor that takes all of the given fields with
* matching (possibly primitive and lenient) type and name.
*/
private static @Nullable List<String> extractConstructorParameterNames(
Constructor<?> constructor, List<Field> fields) {
final Type[] parameterTypes = constructor.getGenericParameterTypes();
List<String> parameterNames = extractExecutableNames(constructor);
if (parameterNames == null) {
return null;
}
final Map<String, Field> fieldMap =
fields.stream()
.collect(
Collectors.toMap(
f -> normalizeAccessorName(f.getName()),
Function.identity()));
// check that all fields are represented in the parameters of the constructor
final List<String> fieldNames = new ArrayList<>();
for (int i = 0; i < parameterNames.size(); i++) {
final String parameterName = normalizeAccessorName(parameterNames.get(i));
final Field field = fieldMap.get(parameterName);
if (field == null) {
return null;
}
final Type fieldType = field.getGenericType();
final Type parameterType = parameterTypes[i];
// we are tolerant here because frameworks such as Avro accept a boxed type even though
// the field is primitive
if (!primitiveToWrapper(parameterType).equals(primitiveToWrapper(fieldType))) {
return null;
}
fieldNames.add(field.getName());
}
return fieldNames;
}
private static @Nullable List<String> extractExecutableNames(Executable executable) {
final int offset;
if (!Modifier.isStatic(executable.getModifiers())) {
// remove "this" as first parameter
offset = 1;
} else {
offset = 0;
}
// by default parameter names are "arg0, arg1, arg2, ..." if compiler flag is not set
// so we need to extract them manually if possible
List<String> parameterNames =
Stream.of(executable.getParameters())
.map(Parameter::getName)
.collect(Collectors.toList());
if (parameterNames.stream().allMatch(n -> n.startsWith("arg"))) {
final ParameterExtractor extractor;
if (executable instanceof Constructor) {
extractor = new ParameterExtractor((Constructor<?>) executable);
} else {
extractor = new ParameterExtractor((Method) executable);
}
getClassReader(executable.getDeclaringClass()).accept(extractor, 0);
final List<String> extractedNames = extractor.getParameterNames();
if (extractedNames.size() == 0) {
return null;
}
// remove "this" and additional local variables
// select less names if class file has not the required information
parameterNames =
extractedNames.subList(
offset,
Math.min(
executable.getParameterCount() + offset,
extractedNames.size()));
}
if (parameterNames.size() != executable.getParameterCount()) {
return null;
}
return parameterNames;
}
private static ClassReader getClassReader(Class<?> cls) {
final String className = cls.getName().replaceFirst("^.*\\.", "") + ".class";
if(ClassPool.exist(cls.getName())){
return new ClassReader(ClassPool.get(cls.getName()).getClassByte());
}
try {
return new ClassReader(cls.getResourceAsStream(className));
} catch (IOException e) {
throw new IllegalStateException("Could not instantiate ClassReader.", e);
}
}
/**
* Extracts the parameter names and descriptors from a constructor or method. Assuming the
* existence of a local variable table.
*
* <p>For example:
*
* <pre>{@code
* public WC(java.lang.String arg0, long arg1) { // <init> //(Ljava/lang/String;J)V
* <localVar:index=0 , name=this , desc=Lorg/apache/flink/WC;, sig=null, start=L1, end=L2>
* <localVar:index=1 , name=word , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=frequency , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal2 , desc=J, sig=null, start=L1, end=L2>
* }
* }</pre>
*/
private static class ParameterExtractor extends ClassVisitor {
private static final int OPCODE = Opcodes.ASM7;
private final String methodDescriptor;
private final List<String> parameterNames = new ArrayList<>();
ParameterExtractor(Constructor<?> constructor) {
super(OPCODE);
methodDescriptor = getConstructorDescriptor(constructor);
}
ParameterExtractor(Method method) {
super(OPCODE);
methodDescriptor = getMethodDescriptor(method);
}
List<String> getParameterNames() {
return parameterNames;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
if (descriptor.equals(methodDescriptor)) {
return new MethodVisitor(OPCODE) {
@Override
public void visitLocalVariable(
String name,
String descriptor,
String signature,
Label start,
Label end,
int index) {
parameterNames.add(name);
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
// --------------------------------------------------------------------------------------------
// Class Assignment and Boxing
//
// copied from o.a.commons.lang3.ClassUtils (commons-lang3:3.3.2)
// --------------------------------------------------------------------------------------------
/**
* Checks if one {@code Class} can be assigned to a variable of another {@code Class}.
*
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this method takes into
* account widenings of primitive classes and {@code null}s.
*
* <p>Primitive widenings allow an int to be assigned to a long, float or double. This method
* returns the correct result for these cases.
*
* <p>{@code Null} may be assigned to any reference type. This method will return {@code true}
* if {@code null} is passed in and the toClass is non-primitive.
*
* <p>Specifically, this method tests whether the type represented by the specified {@code
* Class} parameter can be converted to the type represented by this {@code Class} object via an
* identity conversion widening primitive or widening reference conversion. See <em><a
* href="http://docs.oracle.com/javase/specs/">The Java Language Specification</a></em>,
* sections 5.1.1, 5.1.2 and 5.1.4 for details.
*
* @param cls the Class to check, may be null
* @param toClass the Class to try to assign into, returns false if null
* @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
* @return {@code true} if assignment possible
*/
public static boolean isAssignable(
Class<?> cls, final Class<?> toClass, final boolean autoboxing) {
if (toClass == null) {
return false;
}
// have to check for null, as isAssignableFrom doesn't
if (cls == null) {
return !toClass.isPrimitive();
}
// autoboxing:
if (autoboxing) {
if (cls.isPrimitive() && !toClass.isPrimitive()) {
cls = primitiveToWrapper(cls);
if (cls == null) {
return false;
}
}
if (toClass.isPrimitive() && !cls.isPrimitive()) {
cls = wrapperToPrimitive(cls);
if (cls == null) {
return false;
}
}
}
if (cls.equals(toClass)) {
return true;
}
if (cls.isPrimitive()) {
if (!toClass.isPrimitive()) {
return false;
}
if (Integer.TYPE.equals(cls)) {
return Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Long.TYPE.equals(cls)) {
return Float.TYPE.equals(toClass) || Double.TYPE.equals(toClass);
}
if (Boolean.TYPE.equals(cls)) {
return false;
}
if (Double.TYPE.equals(cls)) {
return false;
}
if (Float.TYPE.equals(cls)) {
return Double.TYPE.equals(toClass);
}
if (Character.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Short.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Byte.TYPE.equals(cls)) {
return Short.TYPE.equals(toClass)
|| Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
// should never get here
return false;
}
return toClass.isAssignableFrom(cls);
}
/** Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. */
private static final Map<Class<?>, Class<?>> primitiveWrapperMap = new HashMap<>();
static {
primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
primitiveWrapperMap.put(Byte.TYPE, Byte.class);
primitiveWrapperMap.put(Character.TYPE, Character.class);
primitiveWrapperMap.put(Short.TYPE, Short.class);
primitiveWrapperMap.put(Integer.TYPE, Integer.class);
primitiveWrapperMap.put(Long.TYPE, Long.class);
primitiveWrapperMap.put(Double.TYPE, Double.class);
primitiveWrapperMap.put(Float.TYPE, Float.class);
primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
}
/** Maps wrapper {@code Class}es to their corresponding primitive types. */
private static final Map<Class<?>, Class<?>> wrapperPrimitiveMap = new HashMap<>();
static {
for (final Class<?> primitiveClass : primitiveWrapperMap.keySet()) {
final Class<?> wrapperClass = primitiveWrapperMap.get(primitiveClass);
if (!primitiveClass.equals(wrapperClass)) {
wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
}
}
}
/**
* Converts the specified primitive Class object to its corresponding wrapper Class object.
*
* <p>NOTE: From v2.2, this method handles {@code Void.TYPE}, returning {@code Void.TYPE}.
*
* @param cls the class to convert, may be null
* @return the wrapper class for {@code cls} or {@code cls} if {@code cls} is not a primitive.
* {@code null} if null input.
* @since 2.1
*/
public static Class<?> primitiveToWrapper(final Class<?> cls) {
Class<?> convertedClass = cls;
if (cls != null && cls.isPrimitive()) {
convertedClass = primitiveWrapperMap.get(cls);
}
return convertedClass;
}
/**
* Converts the specified wrapper class to its corresponding primitive class.
*
* <p>This method is the counter part of {@code primitiveToWrapper()}. If the passed in class is
* a wrapper class for a primitive type, this primitive type will be returned (e.g. {@code
* Integer.TYPE} for {@code Integer.class}). For other classes, or if the parameter is
* <b>null</b>, the return value is <b>null</b>.
*
* @param cls the class to convert, may be <b>null</b>
* @return the corresponding primitive type if {@code cls} is a wrapper class, <b>null</b>
* otherwise
* @see #primitiveToWrapper(Class)
* @since 2.4
*/
public static Class<?> wrapperToPrimitive(final Class<?> cls) {
return wrapperPrimitiveMap.get(cls);
}
// --------------------------------------------------------------------------------------------
private ExtractionUtils() {
// no instantiation
}
}
/*
* 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.flink.table.types.extraction;
import com.dlink.pool.ClassPool;
import org.apache.flink.annotation.Internal;
import org.apache.flink.api.common.typeutils.TypeSerializer;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.catalog.DataTypeFactory;
import org.apache.flink.table.types.DataType;
import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassReader;
import org.apache.flink.shaded.asm7.org.objectweb.asm.ClassVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Label;
import org.apache.flink.shaded.asm7.org.objectweb.asm.MethodVisitor;
import org.apache.flink.shaded.asm7.org.objectweb.asm.Opcodes;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getConstructorDescriptor;
import static org.apache.flink.shaded.asm7.org.objectweb.asm.Type.getMethodDescriptor;
/** Utilities for performing reflection tasks. */
@Internal
public final class ExtractionUtils {
// --------------------------------------------------------------------------------------------
// Methods shared across packages
// --------------------------------------------------------------------------------------------
/** Collects methods of the given name. */
public static List<Method> collectMethods(Class<?> function, String methodName) {
return Arrays.stream(function.getMethods())
.filter(method -> method.getName().equals(methodName))
.sorted(Comparator.comparing(Method::toString)) // for deterministic order
.collect(Collectors.toList());
}
/**
* Checks whether a method/constructor can be called with the given argument classes. This
* includes type widening and vararg. {@code null} is a wildcard.
*
* <p>E.g., {@code (int.class, int.class)} matches {@code f(Object...), f(int, int), f(Integer,
* Object)} and so forth.
*/
public static boolean isInvokable(Executable executable, Class<?>... classes) {
final int m = executable.getModifiers();
if (!Modifier.isPublic(m)) {
return false;
}
final int paramCount = executable.getParameterCount();
final int classCount = classes.length;
// check for enough classes for each parameter
if ((!executable.isVarArgs() && classCount != paramCount)
|| (executable.isVarArgs() && classCount < paramCount - 1)) {
return false;
}
int currentClass = 0;
for (int currentParam = 0; currentParam < paramCount; currentParam++) {
final Class<?> param = executable.getParameterTypes()[currentParam];
// last parameter is a vararg that needs to consume remaining classes
if (currentParam == paramCount - 1 && executable.isVarArgs()) {
final Class<?> paramComponent =
executable.getParameterTypes()[currentParam].getComponentType();
// we have more than 1 classes left so the vararg needs to consume them all
if (classCount - currentClass > 1) {
while (currentClass < classCount
&& ExtractionUtils.isAssignable(
classes[currentClass], paramComponent, true)) {
currentClass++;
}
} else if (currentClass < classCount
&& (parameterMatches(classes[currentClass], param)
|| parameterMatches(classes[currentClass], paramComponent))) {
currentClass++;
}
}
// entire parameter matches
else if (parameterMatches(classes[currentClass], param)) {
currentClass++;
}
}
// check if all classes have been consumed
return currentClass == classCount;
}
private static boolean parameterMatches(Class<?> clz, Class<?> param) {
return clz == null || ExtractionUtils.isAssignable(clz, param, true);
}
/** Creates a method signature string like {@code int eval(Integer, String)}. */
public static String createMethodSignatureString(
String methodName, Class<?>[] parameters, @Nullable Class<?> returnType) {
final StringBuilder builder = new StringBuilder();
if (returnType != null) {
builder.append(returnType.getCanonicalName()).append(" ");
}
builder.append(methodName)
.append(
Stream.of(parameters)
.map(
parameter -> {
// in case we don't know the parameter at this location
// (i.e. for accumulators)
if (parameter == null) {
return "_";
} else {
return parameter.getCanonicalName();
}
})
.collect(Collectors.joining(", ", "(", ")")));
return builder.toString();
}
/**
* Validates the characteristics of a class for a {@link StructuredType} such as accessibility.
*/
public static void validateStructuredClass(Class<?> clazz) {
final int m = clazz.getModifiers();
if (Modifier.isAbstract(m)) {
throw extractionError("Class '%s' must not be abstract.", clazz.getName());
}
if (!Modifier.isPublic(m)) {
throw extractionError("Class '%s' is not public.", clazz.getName());
}
if (clazz.getEnclosingClass() != null
&& (clazz.getDeclaringClass() == null || !Modifier.isStatic(m))) {
throw extractionError(
"Class '%s' is a not a static, globally accessible class.", clazz.getName());
}
}
/**
* Returns the field of a structured type. The logic is as broad as possible to support both
* Java and Scala in different flavors.
*/
public static Field getStructuredField(Class<?> clazz, String fieldName) {
final String normalizedFieldName = fieldName.toUpperCase();
final List<Field> fields = collectStructuredFields(clazz);
for (Field field : fields) {
if (field.getName().toUpperCase().equals(normalizedFieldName)) {
return field;
}
}
throw extractionError(
"Could not find a field named '%s' in class '%s' for structured type.",
fieldName, clazz.getName());
}
/**
* Checks for a field getter of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldGetter(Class<?> clazz, Field field) {
final String normalizedFieldName = normalizeAccessorName(field.getName());
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// get<Name>()
// is<Name>()
// <Name>() for Scala
final String normalizedMethodName = normalizeAccessorName(method.getName());
final boolean hasName =
normalizedMethodName.equals("GET" + normalizedFieldName)
|| normalizedMethodName.equals("IS" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName);
if (!hasName) {
continue;
}
// check return type:
// equal to field type
final Type returnType = method.getGenericReturnType();
final boolean hasReturnType = returnType.equals(field.getGenericType());
if (!hasReturnType) {
continue;
}
// check parameters:
// no parameters
final boolean hasNoParameters = method.getParameterCount() == 0;
if (!hasNoParameters) {
continue;
}
// matching getter found
return Optional.of(method);
}
// no getter found
return Optional.empty();
}
/**
* Checks for a field setters of a structured type. The logic is as broad as possible to support
* both Java and Scala in different flavors.
*/
public static Optional<Method> getStructuredFieldSetter(Class<?> clazz, Field field) {
final String normalizedFieldName = normalizeAccessorName(field.getName());
final List<Method> methods = collectStructuredMethods(clazz);
for (Method method : methods) {
// check name:
// set<Name>(type)
// <Name>(type)
// <Name>_$eq(type) for Scala
final String normalizedMethodName = normalizeAccessorName(method.getName());
final boolean hasName =
normalizedMethodName.equals("SET" + normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName)
|| normalizedMethodName.equals(normalizedFieldName + "$EQ");
if (!hasName) {
continue;
}
// check return type:
// void or the declaring class
final Class<?> returnType = method.getReturnType();
final boolean hasReturnType = returnType == Void.TYPE || returnType == clazz;
if (!hasReturnType) {
continue;
}
// check parameters:
// one parameter that has the same (or primitive) type of the field
final boolean hasParameter =
method.getParameterCount() == 1
&& (method.getGenericParameterTypes()[0].equals(field.getGenericType())
|| primitiveToWrapper(method.getGenericParameterTypes()[0])
.equals(field.getGenericType()));
if (!hasParameter) {
continue;
}
// matching setter found
return Optional.of(method);
}
// no setter found
return Optional.empty();
}
private static String normalizeAccessorName(String name) {
return name.toUpperCase().replaceAll(Pattern.quote("_"), "");
}
/**
* Checks for an invokable constructor matching the given arguments.
*
* @see #isInvokable(Executable, Class[])
*/
public static boolean hasInvokableConstructor(Class<?> clazz, Class<?>... classes) {
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (isInvokable(constructor, classes)) {
return true;
}
}
return false;
}
/** Checks whether a field is directly readable without a getter. */
public static boolean isStructuredFieldDirectlyReadable(Field field) {
final int m = field.getModifiers();
// field is directly readable
return Modifier.isPublic(m);
}
/** Checks whether a field is directly writable without a setter or constructor. */
public static boolean isStructuredFieldDirectlyWritable(Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly writable
return Modifier.isPublic(m);
}
/**
* A minimal version to extract a generic parameter from a given class.
*
* <p>This method should only be used for very specific use cases, in most cases {@link
* DataTypeExtractor#extractFromGeneric(DataTypeFactory, Class, int, Type)} should be more
* appropriate.
*/
public static Optional<Class<?>> extractSimpleGeneric(
Class<?> baseClass, Class<?> clazz, int pos) {
try {
if (clazz.getSuperclass() != baseClass) {
return Optional.empty();
}
final Type t =
((ParameterizedType) clazz.getGenericSuperclass())
.getActualTypeArguments()[pos];
return Optional.ofNullable(toClass(t));
} catch (Exception unused) {
return Optional.empty();
}
}
// --------------------------------------------------------------------------------------------
// Methods intended for this package
// --------------------------------------------------------------------------------------------
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(String message, Object... args) {
return extractionError(null, message, args);
}
/** Helper method for creating consistent exceptions during extraction. */
static ValidationException extractionError(Throwable cause, String message, Object... args) {
return new ValidationException(String.format(message, args), cause);
}
/**
* Collects the partially ordered type hierarchy (i.e. all involved super classes and super
* interfaces) of the given type.
*/
static List<Type> collectTypeHierarchy(Type type) {
Type currentType = type;
Class<?> currentClass = toClass(type);
final List<Type> typeHierarchy = new ArrayList<>();
while (currentClass != null) {
// collect type
typeHierarchy.add(currentType);
// collect super interfaces
for (Type genericInterface : currentClass.getGenericInterfaces()) {
final Class<?> interfaceClass = toClass(genericInterface);
if (interfaceClass != null) {
typeHierarchy.addAll(collectTypeHierarchy(genericInterface));
}
}
currentType = currentClass.getGenericSuperclass();
currentClass = toClass(currentType);
}
return typeHierarchy;
}
/** Converts a {@link Type} to {@link Class} if possible, {@code null} otherwise. */
static @Nullable Class<?> toClass(Type type) {
if (type instanceof Class) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
// this is always a class
return (Class<?>) ((ParameterizedType) type).getRawType();
}
// unsupported: generic arrays, type variables, wildcard types
return null;
}
/** Creates a raw data type. */
@SuppressWarnings({"unchecked", "rawtypes"})
static DataType createRawType(
DataTypeFactory typeFactory,
@Nullable Class<? extends TypeSerializer<?>> rawSerializer,
@Nullable Class<?> conversionClass) {
if (rawSerializer != null) {
return DataTypes.RAW(
(Class) createConversionClass(conversionClass),
instantiateRawSerializer(rawSerializer));
}
return typeFactory.createRawDataType(createConversionClass(conversionClass));
}
static Class<?> createConversionClass(@Nullable Class<?> conversionClass) {
if (conversionClass != null) {
return conversionClass;
}
return Object.class;
}
private static TypeSerializer<?> instantiateRawSerializer(
Class<? extends TypeSerializer<?>> rawSerializer) {
try {
return rawSerializer.newInstance();
} catch (Exception e) {
throw extractionError(
e,
"Cannot instantiate type serializer '%s' for RAW type. "
+ "Make sure the class is publicly accessible and has a default constructor.",
rawSerializer.getName());
}
}
/** Resolves a {@link TypeVariable} using the given type hierarchy if possible. */
static Type resolveVariable(List<Type> typeHierarchy, TypeVariable<?> variable) {
// iterate through hierarchy from top to bottom until type variable gets a non-variable
// assigned
for (int i = typeHierarchy.size() - 1; i >= 0; i--) {
final Type currentType = typeHierarchy.get(i);
if (currentType instanceof ParameterizedType) {
final Type resolvedType =
resolveVariableInParameterizedType(
variable, (ParameterizedType) currentType);
if (resolvedType instanceof TypeVariable) {
// follow type variables transitively
variable = (TypeVariable<?>) resolvedType;
} else if (resolvedType != null) {
return resolvedType;
}
}
}
// unresolved variable
return variable;
}
private static @Nullable Type resolveVariableInParameterizedType(
TypeVariable<?> variable, ParameterizedType currentType) {
final Class<?> currentRaw = (Class<?>) currentType.getRawType();
final TypeVariable<?>[] currentVariables = currentRaw.getTypeParameters();
// search for matching type variable
for (int paramPos = 0; paramPos < currentVariables.length; paramPos++) {
if (typeVariableEquals(variable, currentVariables[paramPos])) {
return currentType.getActualTypeArguments()[paramPos];
}
}
return null;
}
private static boolean typeVariableEquals(
TypeVariable<?> variable, TypeVariable<?> currentVariable) {
return currentVariable.getGenericDeclaration().equals(variable.getGenericDeclaration())
&& currentVariable.getName().equals(variable.getName());
}
/**
* Validates if a given type is not already contained in the type hierarchy of a structured
* type.
*
* <p>Otherwise this would lead to infinite data type extraction cycles.
*/
static void validateStructuredSelfReference(Type t, List<Type> typeHierarchy) {
final Class<?> clazz = toClass(t);
if (clazz != null
&& !clazz.isInterface()
&& clazz != Object.class
&& typeHierarchy.contains(t)) {
throw extractionError(
"Cyclic reference detected for class '%s'. Attributes of structured types must not "
+ "(transitively) reference the structured type itself.",
clazz.getName());
}
}
/** Returns the fields of a class for a {@link StructuredType}. */
static List<Field> collectStructuredFields(Class<?> clazz) {
final List<Field> fields = new ArrayList<>();
while (clazz != Object.class) {
final Field[] declaredFields = clazz.getDeclaredFields();
Stream.of(declaredFields)
.filter(
field -> {
final int m = field.getModifiers();
return !Modifier.isStatic(m) && !Modifier.isTransient(m);
})
.forEach(fields::add);
clazz = clazz.getSuperclass();
}
return fields;
}
/** Validates if a field is properly readable either directly or through a getter. */
static void validateStructuredFieldReadability(Class<?> clazz, Field field) {
// field is accessible
if (isStructuredFieldDirectlyReadable(field)) {
return;
}
// field needs a getter
if (!getStructuredFieldGetter(clazz, field).isPresent()) {
throw extractionError(
"Field '%s' of class '%s' is neither publicly accessible nor does it have "
+ "a corresponding getter method.",
field.getName(), clazz.getName());
}
}
/**
* Checks if a field is mutable or immutable. Returns {@code true} if the field is properly
* mutable. Returns {@code false} if it is properly immutable.
*/
static boolean isStructuredFieldMutable(Class<?> clazz, Field field) {
final int m = field.getModifiers();
// field is immutable
if (Modifier.isFinal(m)) {
return false;
}
// field is directly mutable
if (Modifier.isPublic(m)) {
return true;
}
// field has setters by which it is mutable
if (getStructuredFieldSetter(clazz, field).isPresent()) {
return true;
}
throw extractionError(
"Field '%s' of class '%s' is mutable but is neither publicly accessible nor does it have "
+ "a corresponding setter method.",
field.getName(), clazz.getName());
}
/** Returns the boxed type of a primitive type. */
static Type primitiveToWrapper(Type type) {
if (type instanceof Class) {
return primitiveToWrapper((Class<?>) type);
}
return type;
}
/** Collects all methods that qualify as methods of a {@link StructuredType}. */
static List<Method> collectStructuredMethods(Class<?> clazz) {
final List<Method> methods = new ArrayList<>();
while (clazz != Object.class) {
final Method[] declaredMethods = clazz.getDeclaredMethods();
Stream.of(declaredMethods)
.filter(
field -> {
final int m = field.getModifiers();
return Modifier.isPublic(m)
&& !Modifier.isNative(m)
&& !Modifier.isAbstract(m);
})
.forEach(methods::add);
clazz = clazz.getSuperclass();
}
return methods;
}
/**
* Collects all annotations of the given type defined in the current class or superclasses.
* Duplicates are ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfClass(
Class<T> annotation, Class<?> annotatedClass) {
final List<Class<?>> classHierarchy = new ArrayList<>();
Class<?> currentClass = annotatedClass;
while (currentClass != null) {
classHierarchy.add(currentClass);
currentClass = currentClass.getSuperclass();
}
// convert to top down
Collections.reverse(classHierarchy);
return classHierarchy.stream()
.flatMap(c -> Stream.of(c.getAnnotationsByType(annotation)))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Collects all annotations of the given type defined in the given method. Duplicates are
* ignored.
*/
static <T extends Annotation> Set<T> collectAnnotationsOfMethod(
Class<T> annotation, Method annotatedMethod) {
return new LinkedHashSet<>(Arrays.asList(annotatedMethod.getAnnotationsByType(annotation)));
}
// --------------------------------------------------------------------------------------------
// Parameter Extraction Utilities
// --------------------------------------------------------------------------------------------
/** Result of the extraction in {@link #extractAssigningConstructor(Class, List)}. */
public static class AssigningConstructor {
public final Constructor<?> constructor;
public final List<String> parameterNames;
private AssigningConstructor(Constructor<?> constructor, List<String> parameterNames) {
this.constructor = constructor;
this.parameterNames = parameterNames;
}
}
/**
* Checks whether the given constructor takes all of the given fields with matching (possibly
* primitive) type and name. An assigning constructor can define the order of fields.
*/
public static @Nullable AssigningConstructor extractAssigningConstructor(
Class<?> clazz, List<Field> fields) {
AssigningConstructor foundConstructor = null;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
final boolean qualifyingConstructor =
Modifier.isPublic(constructor.getModifiers())
&& constructor.getParameterTypes().length == fields.size();
if (!qualifyingConstructor) {
continue;
}
final List<String> parameterNames =
extractConstructorParameterNames(constructor, fields);
if (parameterNames != null) {
if (foundConstructor != null) {
throw extractionError(
"Multiple constructors found that assign all fields for class '%s'.",
clazz.getName());
}
foundConstructor = new AssigningConstructor(constructor, parameterNames);
}
}
return foundConstructor;
}
/** Extracts the parameter names of a method if possible. */
static @Nullable List<String> extractMethodParameterNames(Method method) {
return extractExecutableNames(method);
}
/**
* Extracts ordered parameter names from a constructor that takes all of the given fields with
* matching (possibly primitive and lenient) type and name.
*/
private static @Nullable List<String> extractConstructorParameterNames(
Constructor<?> constructor, List<Field> fields) {
final Type[] parameterTypes = constructor.getGenericParameterTypes();
List<String> parameterNames = extractExecutableNames(constructor);
if (parameterNames == null) {
return null;
}
final Map<String, Field> fieldMap =
fields.stream()
.collect(
Collectors.toMap(
f -> normalizeAccessorName(f.getName()),
Function.identity()));
// check that all fields are represented in the parameters of the constructor
final List<String> fieldNames = new ArrayList<>();
for (int i = 0; i < parameterNames.size(); i++) {
final String parameterName = normalizeAccessorName(parameterNames.get(i));
final Field field = fieldMap.get(parameterName);
if (field == null) {
return null;
}
final Type fieldType = field.getGenericType();
final Type parameterType = parameterTypes[i];
// we are tolerant here because frameworks such as Avro accept a boxed type even though
// the field is primitive
if (!primitiveToWrapper(parameterType).equals(primitiveToWrapper(fieldType))) {
return null;
}
fieldNames.add(field.getName());
}
return fieldNames;
}
private static @Nullable List<String> extractExecutableNames(Executable executable) {
final int offset;
if (!Modifier.isStatic(executable.getModifiers())) {
// remove "this" as first parameter
offset = 1;
} else {
offset = 0;
}
// by default parameter names are "arg0, arg1, arg2, ..." if compiler flag is not set
// so we need to extract them manually if possible
List<String> parameterNames =
Stream.of(executable.getParameters())
.map(Parameter::getName)
.collect(Collectors.toList());
if (parameterNames.stream().allMatch(n -> n.startsWith("arg"))) {
final ParameterExtractor extractor;
if (executable instanceof Constructor) {
extractor = new ParameterExtractor((Constructor<?>) executable);
} else {
extractor = new ParameterExtractor((Method) executable);
}
getClassReader(executable.getDeclaringClass()).accept(extractor, 0);
final List<String> extractedNames = extractor.getParameterNames();
if (extractedNames.size() == 0) {
return null;
}
// remove "this" and additional local variables
// select less names if class file has not the required information
parameterNames =
extractedNames.subList(
offset,
Math.min(
executable.getParameterCount() + offset,
extractedNames.size()));
}
if (parameterNames.size() != executable.getParameterCount()) {
return null;
}
return parameterNames;
}
private static ClassReader getClassReader(Class<?> cls) {
final String className = cls.getName().replaceFirst("^.*\\.", "") + ".class";
if(ClassPool.exist(cls.getName())){
return new ClassReader(ClassPool.get(cls.getName()).getClassByte());
}
try (InputStream i = cls.getResourceAsStream(className)) {
return new ClassReader(i);
} catch (IOException e) {
throw new IllegalStateException("Could not instantiate ClassReader.", e);
}
}
/**
* Extracts the parameter names and descriptors from a constructor or method. Assuming the
* existence of a local variable table.
*
* <p>For example:
*
* <pre>{@code
* public WC(java.lang.String arg0, long arg1) { // <init> //(Ljava/lang/String;J)V
* <localVar:index=0 , name=this , desc=Lorg/apache/flink/WC;, sig=null, start=L1, end=L2>
* <localVar:index=1 , name=word , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=frequency , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal , desc=J, sig=null, start=L1, end=L2>
* <localVar:index=2 , name=otherLocal2 , desc=J, sig=null, start=L1, end=L2>
* }
* }</pre>
*/
private static class ParameterExtractor extends ClassVisitor {
private static final int OPCODE = Opcodes.ASM7;
private final String methodDescriptor;
private final List<String> parameterNames = new ArrayList<>();
ParameterExtractor(Constructor<?> constructor) {
super(OPCODE);
methodDescriptor = getConstructorDescriptor(constructor);
}
ParameterExtractor(Method method) {
super(OPCODE);
methodDescriptor = getMethodDescriptor(method);
}
List<String> getParameterNames() {
return parameterNames;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
if (descriptor.equals(methodDescriptor)) {
return new MethodVisitor(OPCODE) {
@Override
public void visitLocalVariable(
String name,
String descriptor,
String signature,
Label start,
Label end,
int index) {
parameterNames.add(name);
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
// --------------------------------------------------------------------------------------------
// Class Assignment and Boxing
//
// copied from o.a.commons.lang3.ClassUtils (commons-lang3:3.3.2)
// --------------------------------------------------------------------------------------------
/**
* Checks if one {@code Class} can be assigned to a variable of another {@code Class}.
*
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this method takes into
* account widenings of primitive classes and {@code null}s.
*
* <p>Primitive widenings allow an int to be assigned to a long, float or double. This method
* returns the correct result for these cases.
*
* <p>{@code Null} may be assigned to any reference type. This method will return {@code true}
* if {@code null} is passed in and the toClass is non-primitive.
*
* <p>Specifically, this method tests whether the type represented by the specified {@code
* Class} parameter can be converted to the type represented by this {@code Class} object via an
* identity conversion widening primitive or widening reference conversion. See <em><a
* href="http://docs.oracle.com/javase/specs/">The Java Language Specification</a></em>,
* sections 5.1.1, 5.1.2 and 5.1.4 for details.
*
* @param cls the Class to check, may be null
* @param toClass the Class to try to assign into, returns false if null
* @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
* @return {@code true} if assignment possible
*/
public static boolean isAssignable(
Class<?> cls, final Class<?> toClass, final boolean autoboxing) {
if (toClass == null) {
return false;
}
// have to check for null, as isAssignableFrom doesn't
if (cls == null) {
return !toClass.isPrimitive();
}
// autoboxing:
if (autoboxing) {
if (cls.isPrimitive() && !toClass.isPrimitive()) {
cls = primitiveToWrapper(cls);
if (cls == null) {
return false;
}
}
if (toClass.isPrimitive() && !cls.isPrimitive()) {
cls = wrapperToPrimitive(cls);
if (cls == null) {
return false;
}
}
}
if (cls.equals(toClass)) {
return true;
}
if (cls.isPrimitive()) {
if (!toClass.isPrimitive()) {
return false;
}
if (Integer.TYPE.equals(cls)) {
return Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Long.TYPE.equals(cls)) {
return Float.TYPE.equals(toClass) || Double.TYPE.equals(toClass);
}
if (Boolean.TYPE.equals(cls)) {
return false;
}
if (Double.TYPE.equals(cls)) {
return false;
}
if (Float.TYPE.equals(cls)) {
return Double.TYPE.equals(toClass);
}
if (Character.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Short.TYPE.equals(cls)) {
return Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
if (Byte.TYPE.equals(cls)) {
return Short.TYPE.equals(toClass)
|| Integer.TYPE.equals(toClass)
|| Long.TYPE.equals(toClass)
|| Float.TYPE.equals(toClass)
|| Double.TYPE.equals(toClass);
}
// should never get here
return false;
}
return toClass.isAssignableFrom(cls);
}
/** Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. */
private static final Map<Class<?>, Class<?>> primitiveWrapperMap = new HashMap<>();
static {
primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
primitiveWrapperMap.put(Byte.TYPE, Byte.class);
primitiveWrapperMap.put(Character.TYPE, Character.class);
primitiveWrapperMap.put(Short.TYPE, Short.class);
primitiveWrapperMap.put(Integer.TYPE, Integer.class);
primitiveWrapperMap.put(Long.TYPE, Long.class);
primitiveWrapperMap.put(Double.TYPE, Double.class);
primitiveWrapperMap.put(Float.TYPE, Float.class);
primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
}
/** Maps wrapper {@code Class}es to their corresponding primitive types. */
private static final Map<Class<?>, Class<?>> wrapperPrimitiveMap = new HashMap<>();
static {
for (final Class<?> primitiveClass : primitiveWrapperMap.keySet()) {
final Class<?> wrapperClass = primitiveWrapperMap.get(primitiveClass);
if (!primitiveClass.equals(wrapperClass)) {
wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
}
}
}
/**
* Converts the specified primitive Class object to its corresponding wrapper Class object.
*
* <p>NOTE: From v2.2, this method handles {@code Void.TYPE}, returning {@code Void.TYPE}.
*
* @param cls the class to convert, may be null
* @return the wrapper class for {@code cls} or {@code cls} if {@code cls} is not a primitive.
* {@code null} if null input.
* @since 2.1
*/
public static Class<?> primitiveToWrapper(final Class<?> cls) {
Class<?> convertedClass = cls;
if (cls != null && cls.isPrimitive()) {
convertedClass = primitiveWrapperMap.get(cls);
}
return convertedClass;
}
/**
* Converts the specified wrapper class to its corresponding primitive class.
*
* <p>This method is the counter part of {@code primitiveToWrapper()}. If the passed in class is
* a wrapper class for a primitive type, this primitive type will be returned (e.g. {@code
* Integer.TYPE} for {@code Integer.class}). For other classes, or if the parameter is
* <b>null</b>, the return value is <b>null</b>.
*
* @param cls the class to convert, may be <b>null</b>
* @return the corresponding primitive type if {@code cls} is a wrapper class, <b>null</b>
* otherwise
* @see #primitiveToWrapper(Class)
* @since 2.4
*/
public static Class<?> wrapperToPrimitive(final Class<?> cls) {
return wrapperPrimitiveMap.get(cls);
}
// --------------------------------------------------------------------------------------------
private ExtractionUtils() {
// no instantiation
}
}
package com.dlink.pool;
import com.dlink.assertion.Asserts;
import lombok.Getter;
import lombok.Setter;
/**
* ClassEntity
*
* @author wenmo
* @since 2022/1/12 23:52
*/
@Getter
@Setter
public class ClassEntity {
private String name;
private String code;
private byte[] classByte;
public ClassEntity(String name, String code) {
this.name = name;
this.code = code;
}
public ClassEntity(String name, String code, byte[] classByte) {
this.name = name;
this.code = code;
this.classByte = classByte;
}
public static ClassEntity build(String name, String code){
return new ClassEntity(name,code);
}
public boolean equals(ClassEntity entity) {
if (Asserts.isEquals(name, entity.getName()) && Asserts.isEquals(code, entity.getCode())){
return true;
}else{
return false;
}
}
}
package com.dlink.pool;
import java.util.List;
import java.util.Vector;
/**
* ClassPool
*
* @author wenmo
* @since 2022/1/12 23:52
*/
public class ClassPool {
private static volatile List<ClassEntity> classList = new Vector<>();
public static boolean exist(String name) {
for (ClassEntity executorEntity : classList) {
if (executorEntity.getName().equals(name)) {
return true;
}
}
return false;
}
public static boolean exist(ClassEntity entity) {
for (ClassEntity executorEntity : classList) {
if (executorEntity.equals(entity)) {
return true;
}
}
return false;
}
public static Integer push(ClassEntity executorEntity){
if(exist(executorEntity.getName())){
remove(executorEntity.getName());
}
classList.add(executorEntity);
return classList.size();
}
public static Integer remove(String name) {
int count = classList.size();
for (int i = 0; i < classList.size(); i++) {
if (name.equals(classList.get(i).getName())) {
classList.remove(i);
break;
}
}
return count - classList.size();
}
public static ClassEntity get(String name) {
for (ClassEntity executorEntity : classList) {
if (executorEntity.getName().equals(name)) {
return executorEntity;
}
}
return null;
}
}
...@@ -35,6 +35,11 @@ ...@@ -35,6 +35,11 @@
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.9</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
......
...@@ -9,6 +9,7 @@ import com.dlink.explainer.trans.TransGenerator; ...@@ -9,6 +9,7 @@ import com.dlink.explainer.trans.TransGenerator;
import com.dlink.interceptor.FlinkInterceptor; import com.dlink.interceptor.FlinkInterceptor;
import com.dlink.job.JobParam; import com.dlink.job.JobParam;
import com.dlink.job.StatementParam; import com.dlink.job.StatementParam;
import com.dlink.model.SystemConfiguration;
import com.dlink.parser.SqlType; import com.dlink.parser.SqlType;
import com.dlink.result.ExplainResult; import com.dlink.result.ExplainResult;
import com.dlink.result.SqlExplainResult; import com.dlink.result.SqlExplainResult;
...@@ -162,7 +163,7 @@ public class Explainer { ...@@ -162,7 +163,7 @@ public class Explainer {
} }
} }
if (inserts.size() > 0) { if (inserts.size() > 0) {
String sqlSet = String.join(FlinkSQLConstant.SEPARATOR, inserts); String sqlSet = String.join(";\r\n ", inserts);
try { try {
record.setExplain(executor.explainStatementSet(inserts)); record.setExplain(executor.explainStatementSet(inserts));
record.setParseTrue(true); record.setParseTrue(true);
......
...@@ -19,6 +19,8 @@ import com.dlink.gateway.result.TestResult; ...@@ -19,6 +19,8 @@ import com.dlink.gateway.result.TestResult;
import com.dlink.interceptor.FlinkInterceptor; import com.dlink.interceptor.FlinkInterceptor;
import com.dlink.model.SystemConfiguration; import com.dlink.model.SystemConfiguration;
import com.dlink.parser.SqlType; import com.dlink.parser.SqlType;
import com.dlink.pool.ClassEntity;
import com.dlink.pool.ClassPool;
import com.dlink.result.*; import com.dlink.result.*;
import com.dlink.session.ExecutorEntity; import com.dlink.session.ExecutorEntity;
import com.dlink.session.SessionConfig; import com.dlink.session.SessionConfig;
...@@ -26,6 +28,7 @@ import com.dlink.session.SessionInfo; ...@@ -26,6 +28,7 @@ import com.dlink.session.SessionInfo;
import com.dlink.session.SessionPool; import com.dlink.session.SessionPool;
import com.dlink.trans.Operations; import com.dlink.trans.Operations;
import com.dlink.utils.SqlUtil; import com.dlink.utils.SqlUtil;
import com.dlink.utils.UDFUtil;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.flink.configuration.CoreOptions; import org.apache.flink.configuration.CoreOptions;
import org.apache.flink.configuration.DeploymentOptions; import org.apache.flink.configuration.DeploymentOptions;
...@@ -43,6 +46,8 @@ import java.util.ArrayList; ...@@ -43,6 +46,8 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* JobManager * JobManager
...@@ -491,4 +496,22 @@ public class JobManager { ...@@ -491,4 +496,22 @@ public class JobManager {
sb.append(statement); sb.append(statement);
return sb.toString(); return sb.toString();
} }
public static List<String> getUDFClassName(String statement){
Pattern pattern = Pattern.compile("function (.*?)'(.*?)'", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(statement);
List<String> classNameList = new ArrayList<>();
while (matcher.find()) {
classNameList.add(matcher.group(2));
}
return classNameList;
}
public static void initUDF(String className,String code){
if(ClassPool.exist(ClassEntity.build(className,code))){
UDFUtil.initClassLoader(className);
}else{
UDFUtil.buildClass(code);
}
}
} }
package com.dlink.utils;
import javax.tools.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* CustomStringJavaCompiler
*
* @author wenmo
* @since 2021/12/28 22:46
*/
public class CustomStringJavaCompiler {
//类全名
private String fullClassName;
private String sourceCode;
//存放编译之后的字节码(key:类全名,value:编译之后输出的字节码)
private Map<String, ByteJavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();
//获取java的编译器
private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//存放编译过程中输出的信息
private DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>();
//编译耗时(单位ms)
private long compilerTakeTime;
public String getFullClassName() {
return fullClassName;
}
public ByteJavaFileObject getJavaFileObjectMap(String name) {
return javaFileObjectMap.get(name);
}
public CustomStringJavaCompiler(String sourceCode) {
this.sourceCode = sourceCode;
this.fullClassName = getFullClassName(sourceCode);
}
/**
* 编译字符串源代码,编译失败在 diagnosticsCollector 中获取提示信息
*
* @return true:编译成功 false:编译失败
*/
public boolean compiler() {
long startTime = System.currentTimeMillis();
//标准的内容管理器,更换成自己的实现,覆盖部分方法
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null);
JavaFileManager javaFileManager = new StringJavaFileManage(standardFileManager);
//构造源代码对象
JavaFileObject javaFileObject = new StringJavaFileObject(fullClassName, sourceCode);
//获取一个编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnosticsCollector, null, null, Arrays.asList(javaFileObject));
//设置编译耗时
compilerTakeTime = System.currentTimeMillis() - startTime;
return task.call();
}
/**
* @return 编译信息(错误 警告)
*/
public String getCompilerMessage() {
StringBuilder sb = new StringBuilder();
List<Diagnostic<? extends JavaFileObject>> diagnostics = diagnosticsCollector.getDiagnostics();
for (Diagnostic diagnostic : diagnostics) {
sb.append(diagnostic.toString()).append("\r\n");
}
return sb.toString();
}
public long getCompilerTakeTime() {
return compilerTakeTime;
}
/**
* 获取类的全名称
*
* @param sourceCode 源码
* @return 类的全名称
*/
public static String getFullClassName(String sourceCode) {
String className = "";
Pattern pattern = Pattern.compile("package\\s+\\S+\\s*;");
Matcher matcher = pattern.matcher(sourceCode);
if (matcher.find()) {
className = matcher.group().replaceFirst("package", "").replace(";", "").trim() + ".";
}
pattern = Pattern.compile("class\\s+(\\S+)\\s+");
matcher = pattern.matcher(sourceCode);
if (matcher.find()) {
className += matcher.group(1).trim();
}
return className;
}
/**
* 自定义一个字符串的源码对象
*/
private class StringJavaFileObject extends SimpleJavaFileObject {
//等待编译的源码字段
private String contents;
//java源代码 => StringJavaFileObject对象 的时候使用
public StringJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
}
//字符串源码会调用该方法
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return contents;
}
}
/**
* 自定义一个编译之后的字节码对象
*/
public class ByteJavaFileObject extends SimpleJavaFileObject {
//存放编译后的字节码
private ByteArrayOutputStream outPutStream;
public ByteJavaFileObject(String className, Kind kind) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), kind);
}
//StringJavaFileManage 编译之后的字节码输出会调用该方法(把字节码输出到outputStream)
@Override
public OutputStream openOutputStream() {
outPutStream = new ByteArrayOutputStream();
return outPutStream;
}
//在类加载器加载的时候需要用到
public byte[] getCompiledBytes() {
return outPutStream.toByteArray();
}
}
/**
* 自定义一个JavaFileManage来控制编译之后字节码的输出位置
*/
private class StringJavaFileManage extends ForwardingJavaFileManager {
StringJavaFileManage(JavaFileManager fileManager) {
super(fileManager);
}
//获取输出的文件对象,它表示给定位置处指定类型的指定类。
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
ByteJavaFileObject javaFileObject = new ByteJavaFileObject(className, kind);
javaFileObjectMap.put(className, javaFileObject);
return javaFileObject;
}
}
}
package com.dlink.utils;
import com.dlink.pool.ClassEntity;
import com.dlink.pool.ClassPool;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.control.CompilerConfiguration;
/**
* UDFUtil
*
* @author wenmo
* @since 2021/12/27 23:25
*/
public class UDFUtil {
public static void buildClass(String code){
CustomStringJavaCompiler compiler = new CustomStringJavaCompiler(code);
boolean res = compiler.compiler();
if (res) {
String className = compiler.getFullClassName();
byte[] compiledBytes = compiler.getJavaFileObjectMap(className).getCompiledBytes();
ClassPool.push(new ClassEntity(className,code,compiledBytes));
System.out.println("编译成功");
System.out.println("compilerTakeTime:" + compiler.getCompilerTakeTime());
initClassLoader(className);
} else {
System.out.println("编译失败");
System.out.println(compiler.getCompilerMessage());
}
}
public static void initClassLoader(String name){
ClassEntity classEntity = ClassPool.get(name);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
CompilerConfiguration config = new CompilerConfiguration();
config.setSourceEncoding("UTF-8");
GroovyClassLoader groovyClassLoader = new GroovyClassLoader(contextClassLoader, config);
groovyClassLoader.setShouldRecompile(true);
groovyClassLoader.defineClass(classEntity.getName(),classEntity.getClassByte());
Thread.currentThread().setContextClassLoader(groovyClassLoader);
// Class<?> clazz = groovyClassLoader.parseClass(codeSource,"com.dlink.ud.udf.SubstringFunction");
}
}
...@@ -19,4 +19,8 @@ public interface FlinkSQLConstant { ...@@ -19,4 +19,8 @@ public interface FlinkSQLConstant {
* DML 类型 * DML 类型
*/ */
String DML = "DML"; String DML = "DML";
/**
* 片段 Fragments 标识
*/
String FRAGMENTS = ":=";
} }
package com.dlink.executor; package com.dlink.executor;
import com.dlink.assertion.Asserts;
import com.dlink.constant.FlinkSQLConstant;
import com.dlink.model.SystemConfiguration;
import org.apache.flink.table.api.DataTypes; import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.ExpressionParserException; import org.apache.flink.table.api.ExpressionParserException;
import org.apache.flink.table.api.Table; import org.apache.flink.table.api.Table;
...@@ -148,23 +151,24 @@ public final class SqlManager { ...@@ -148,23 +151,24 @@ public final class SqlManager {
* @throws ExpressionParserException if the name of the variable under the given sql failed. * @throws ExpressionParserException if the name of the variable under the given sql failed.
*/ */
public String parseVariable(String statement) { public String parseVariable(String statement) {
if (statement == null || "".equals(statement)) { if (Asserts.isNullString(statement)) {
return statement; return statement;
} }
String[] strs = statement.split(";"); String[] strs = statement.split(SystemConfiguration.getInstances().getSqlSeparator());
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int i = 0; i < strs.length; i++) { for (int i = 0; i < strs.length; i++) {
String str = strs[i].trim(); String str = strs[i];
if (str.length() == 0) { if (str.trim().length() == 0) {
continue; continue;
} }
if (str.contains(":=")) { str = strs[i];
String[] strs2 = str.split(":="); if (str.contains(FlinkSQLConstant.FRAGMENTS)) {
String[] strs2 = str.split(FlinkSQLConstant.FRAGMENTS);
if (strs2.length >= 2) { if (strs2.length >= 2) {
if (strs2[0].length() == 0) { if (strs2[0].length() == 0) {
throw new ExpressionParserException("Illegal variable name."); throw new ExpressionParserException("Illegal variable name.");
} }
String valueString = str.substring(str.indexOf(":=") + 2); String valueString = str.substring(str.indexOf(FlinkSQLConstant.FRAGMENTS) + 2);
this.registerSqlFragment(strs2[0], replaceVariable(valueString)); this.registerSqlFragment(strs2[0], replaceVariable(valueString));
} else { } else {
throw new ExpressionParserException("Illegal variable definition."); throw new ExpressionParserException("Illegal variable definition.");
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>
<!-- <scope>test</scope>--> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
"dependencies": { "dependencies": {
"@ant-design/charts": "^1.3.4", "@ant-design/charts": "^1.3.4",
"@ant-design/icons": "^4.5.0", "@ant-design/icons": "^4.5.0",
"@ant-design/plots": "^1.0.7",
"@ant-design/pro-descriptions": "^1.6.8", "@ant-design/pro-descriptions": "^1.6.8",
"@ant-design/pro-form": "^1.18.3", "@ant-design/pro-form": "^1.18.3",
"@ant-design/pro-layout": "^6.18.0", "@ant-design/pro-layout": "^6.18.0",
......
@import '~antd/es/style/themes/default.less';
.form_setting{
padding-left: 10px;
}
.form_item{
margin-bottom: 5px;
}
import {Button, Tag, Row, Col, Form, Select, Empty, Switch} from "antd";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
import styles from "./index.less";
import {FireOutlined, SearchOutlined, RedoOutlined, InfoCircleOutlined} from '@ant-design/icons';
import {useEffect, useState} from "react";
import React from "react";
const {Option} = Select;
export type BarChartConfig = {
isGroup: boolean,
isStack: boolean,
isPercent: boolean,
xField: string,
yField: string,
seriesField?: string,
label?: { },
};
export type BarChartProps = {
onChange: (values: Partial<BarChartConfig>) => void;
data: [];
column: [];
};
const BarChartSetting: React.FC<BarChartProps> = (props) => {
const {current,column,onChange: handleChange,dispatch} = props;
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(current.console.chart);
}, [current.console.chart]);
const onValuesChange = (change: any, all: any) => {
let config: BarChartConfig = {
isGroup: all.isGroup,
isStack: all.isStack,
isPercent: all.isPercent,
xField: all.xField?all.xField:column[0],
yField: all.yField?all.yField:column.length>1?column[1]:column[0],
label: {
position: 'middle',
content: (item) => {
return item[all.xField];
},
style: {
fill: '#fff',
},
},
};
if(all.seriesField){
config.seriesField = all.seriesField;
}
handleChange(config);
};
const getColumnOptions = () => {
const itemList = [];
for (const item of column) {
itemList.push(<Option key={item} value={item} label={item}>
{item}
</Option>)
}
return itemList;
};
return (
<>
<Form
form={form}
className={styles.form_setting}
onValuesChange={onValuesChange}
>
<Row>
<Col span={12}>
<Form.Item
label="x 轴" className={styles.form_item} name="xField"
>
{column&&column.length > 0 ? (
<Select allowClear showSearch
defaultValue={column[0]} value={column[0]}>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="y 轴" className={styles.form_item} name="yField"
>
{column&&column.length > 1 ? (
<Select allowClear showSearch
defaultValue={column[1]} value={column[1]}>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<Form.Item
label="分组字段" className={styles.form_item} name="seriesField"
>
{column&&column.length > 0 ? (
<Select allowClear showSearch>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="分组" className={styles.form_item} name="isGroup" valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<Form.Item
label="堆叠" className={styles.form_item} name="isStack" valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="百分比" className={styles.form_item} name="isPercent" valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
</Row>
</Form>
</>
);
};
export default connect(({ Studio }: { Studio: StateType }) => ({
current: Studio.current,
result: Studio.result,
}))(BarChartSetting);
@import '~antd/es/style/themes/default.less';
.form_setting{
padding-left: 10px;
}
.form_item{
margin-bottom: 5px;
}
import {Button, Tag, Row, Col, Form, Select, Empty, Switch} from "antd";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
import styles from "./index.less";
import {FireOutlined, SearchOutlined, RedoOutlined, InfoCircleOutlined} from '@ant-design/icons';
import {useEffect, useState} from "react";
import React from "react";
const {Option} = Select;
export type LineChartConfig = {
padding: string,
xField: string,
yField: string,
seriesField?: string,
stepType?: string,
xAxis?: {
type?: string,
},
slider?: {},
};
export type LineChartProps = {
onChange: (values: Partial<LineChartConfig>) => void;
data: [];
column: [];
};
const LineChartSetting: React.FC<LineChartProps> = (props) => {
const {current,column,onChange: handleChange,dispatch} = props;
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(current.console.chart);
}, [current.console.chart]);
const onValuesChange = (change: any, all: any) => {
let config: LineChartConfig = {
padding: 'auto',
xField: all.xField?all.xField:column[0],
yField: all.yField?all.yField:column.length>1?column[1]:column[0],
};
if(all.seriesField){
config.seriesField = all.seriesField;
}
if(all.openStepType){
config.stepType = 'hv';
}
if(all.openSlider){
config.slider = {
start: 0,
end: 0.5,
};
}
handleChange(config);
};
const getColumnOptions = () => {
const itemList = [];
for (const item of column) {
itemList.push(<Option key={item} value={item} label={item}>
{item}
</Option>)
}
return itemList;
};
return (
<>
<Form
form={form}
className={styles.form_setting}
onValuesChange={onValuesChange}
>
<Row>
<Col span={12}>
<Form.Item
label="x 轴" className={styles.form_item} name="xField"
>
{column&&column.length > 0 ? (
<Select allowClear showSearch
defaultValue={column[0]} value={column[0]}>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="y 轴" className={styles.form_item} name="yField"
>
{column&&column.length > 1 ? (
<Select allowClear showSearch
defaultValue={column[1]} value={column[1]}>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<Form.Item
label="分组字段" className={styles.form_item} name="seriesField"
>
{column&&column.length > 0 ? (
<Select allowClear showSearch>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="缩略轴" className={styles.form_item} name="openSlider" valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<Form.Item
label="阶梯线" className={styles.form_item} name="openStepType" valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
</Row>
</Form>
</>
);
};
export default connect(({ Studio }: { Studio: StateType }) => ({
current: Studio.current,
result: Studio.result,
}))(LineChartSetting);
@import '~antd/es/style/themes/default.less';
.form_setting{
padding-left: 10px;
}
.form_item{
margin-bottom: 5px;
}
import {Button, Tag, Row, Col, Form, Select, Empty, Switch} from "antd";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
import styles from "./index.less";
import {FireOutlined, SearchOutlined, RedoOutlined, InfoCircleOutlined} from '@ant-design/icons';
import {useEffect, useState} from "react";
import React from "react";
const {Option} = Select;
export type PieChartConfig = {
angleField: string,
colorField: string,
label: {},
interactions: [],
};
export type PieChartProps = {
onChange: (values: Partial<PieChartConfig>) => void;
data: [];
column: [];
};
const PieChartSetting: React.FC<PieChartProps> = (props) => {
const {current,column,onChange: handleChange,dispatch} = props;
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(current.console.chart);
}, [current.console.chart]);
const onValuesChange = (change: any, all: any) => {
let config: PieChartConfig = {
angleField: all.angleField?all.angleField:column[0],
colorField: all.colorField?all.colorField:column.length>1?column[1]:column[0],
label: {
type: 'inner',
offset: '-30%',
content: ({ percent }) => `${(percent * 100).toFixed(0)}%`,
style: {
fontSize: 14,
textAlign: 'center',
},
},
interactions: [
{
type: 'element-active',
},
],
};
handleChange(config);
};
const getColumnOptions = () => {
const itemList = [];
for (const item of column) {
itemList.push(<Option key={item} value={item} label={item}>
{item}
</Option>)
}
return itemList;
};
return (
<>
<Form
form={form}
className={styles.form_setting}
onValuesChange={onValuesChange}
>
<Row>
<Col span={12}>
<Form.Item
label="弧轴" className={styles.form_item} name="angleField"
>
{column&&column.length > 0 ? (
<Select allowClear showSearch
defaultValue={column[0]} value={column[0]}>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="颜色" className={styles.form_item} name="colorField"
>
{column&&column.length > 1 ? (
<Select allowClear showSearch
defaultValue={column[1]} value={column[1]}>
{getColumnOptions()}
</Select>):(<Select allowClear showSearch>
{column&&getColumnOptions()}
</Select>)}
</Form.Item>
</Col>
</Row>
</Form>
</>
);
};
export default connect(({ Studio }: { Studio: StateType }) => ({
current: Studio.current,
result: Studio.result,
}))(PieChartSetting);
@import '~antd/es/style/themes/default.less';
.form_setting{
padding-left: 10px;
}
.form_item{
margin-bottom: 5px;
}
import {Button, Tag,Row, Col,Form,Select, Empty} from "antd";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
import styles from "./index.less";
import {RedoOutlined} from '@ant-design/icons';
import {CHART, isSql} from "@/components/Studio/conf";
import { Line,Bar,Pie } from '@ant-design/plots';
import React, {useEffect, useState} from "react";
import LineChartSetting from "./LineChartSetting";
import BarChartSetting from "./BarChartSetting";
import PieChartSetting from "./PieChartSetting";
import {showJobData} from "@/components/Studio/StudioEvent/DQL";
import {Dispatch} from "@@/plugin-dva/connect";
const {Option} = Select;
const Chart = (props:any) => {
const {current,result,height,dispatch} = props;
const [config, setConfig] = useState({});
const [type, setType] = useState<string>(CHART.LINE);
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(current.console.chart);
}, [current.console.chart]);
const toRebuild = () => {
if(!isSql(current.task.dialect)){
showJobData(current.console.result.jobId,dispatch);
}
};
const onValuesChange = (change: any, all: any) => {
if(change.type){
setType(change.type);
props.saveChart({type:change.type});
}
};
const renderChartSetting = () => {
if(!current.console.chart||!current.console.result.result){
return undefined;
}
switch (type){
case CHART.LINE:
return <LineChartSetting column={current.console.result.result.columns} onChange={(value) => {
setConfig(value);
props.saveChart({...value,type: current.console.chart.type});
}} />;
case CHART.BAR:
return <BarChartSetting column={current.console.result.result.columns} onChange={(value) => {
setConfig(value);
props.saveChart({...value,type: current.console.chart.type});
}} />;
case CHART.PIE:
return <PieChartSetting column={current.console.result.result.columns} onChange={(value) => {
setConfig(value);
props.saveChart({...value,type: current.console.chart.type});
}} />;
default:
return <LineChartSetting column={current.console.result.result.columns} onChange={(value) => {
setConfig(value);
props.saveChart({...value,type: current.console.chart.type});
}} />
}
};
const renderChartContent = () => {
if(!current.console.result.result||!current.console.result.result.columns){
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}
switch (current.console.chart.type){
case CHART.LINE:
return <Line data={current.console.result.result.rowData} {...config} />;
case CHART.BAR:
return <Bar data={current.console.result.result.rowData} {...config} />;
case CHART.PIE:
if(config.angleField){
return <Pie data={current.console.result.result.rowData} {...config} />;
} else {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}
default:
return <Line data={current.console.result.result.rowData} {...config} />;
}
};
return (
<div style={{width: '100%'}}>
<Row>
<Col span={16} style={{padding:'20px'}}>
{renderChartContent()}
</Col>
<Col span={8}>
<Form
form={form}
className={styles.form_setting}
onValuesChange={onValuesChange}
>
<Row>
<Col span={12}>
<Form.Item
label="图形类型" className={styles.form_item} name="type"
>
<Select defaultValue={CHART.LINE} value={CHART.LINE}>
<Option value={CHART.LINE}>{CHART.LINE}</Option>
<Option value={CHART.BAR}>{CHART.BAR}</Option>
<Option value={CHART.PIE}>{CHART.PIE}</Option>
</Select>
</Form.Item>
</Col>
{ !isSql(current.task.dialect) ? <Col span={12}>
<Button type="primary" onClick={toRebuild} icon={<RedoOutlined />}>
刷新数据
</Button>
</Col>:undefined}
</Row>
</Form>
{renderChartSetting()}
</Col>
</Row>
</div>
);
};
const mapDispatchToProps = (dispatch: Dispatch)=>({
saveChart:(chart: any)=>dispatch({
type: "Studio/saveChart",
payload: chart,
}),
})
export default connect(({ Studio }: { Studio: StateType }) => ({
current: Studio.current,
result: Studio.result,
}),mapDispatchToProps)(Chart);
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
import styles from "./index.less"; import styles from "./index.less";
import {showJobData} from "@/components/Studio/StudioEvent/DQL"; import {showJobData} from "@/components/Studio/StudioEvent/DQL";
import StudioPreview from "../StudioPreview"; import StudioPreview from "../StudioPreview";
import {getJobData} from "@/pages/FlinkSqlStudio/service";
const { Title, Paragraph, Text, Link } = Typography; const { Title, Paragraph, Text, Link } = Typography;
...@@ -61,14 +62,19 @@ const StudioHistory = (props: any) => { ...@@ -61,14 +62,19 @@ const StudioHistory = (props: any) => {
const [row, setRow] = useState<HistoryItem>(); const [row, setRow] = useState<HistoryItem>();
const [config,setConfig] = useState<HistoryConfig>(); const [config,setConfig] = useState<HistoryConfig>();
const [type,setType] = useState<number>(); const [type,setType] = useState<number>();
const [result,setResult] = useState<{}>();
const showDetail=(row:HistoryItem,type:number)=>{ const showDetail=(row:HistoryItem,type:number)=>{
setRow(row); setRow(row);
setModalVisit(true); setModalVisit(true);
setType(type); setType(type);
setConfig(JSON.parse(row.config)); setConfig(JSON.parse(row.config));
if(type==3){ if(type===3){
showJobData(row.jobId,dispatch) // showJobData(row.jobId,dispatch)
const res = getJobData(row.jobId);
res.then((resd)=>{
setResult(resd.datas);
});
} }
}; };
...@@ -341,7 +347,7 @@ const StudioHistory = (props: any) => { ...@@ -341,7 +347,7 @@ const StudioHistory = (props: any) => {
</Tag> </Tag>
</ProDescriptions.Item> </ProDescriptions.Item>
<ProDescriptions.Item span={2} > <ProDescriptions.Item span={2} >
<StudioPreview style={{width: '100%'}}/> <StudioPreview result={result} style={{width: '100%'}}/>
</ProDescriptions.Item> </ProDescriptions.Item>
</ProDescriptions> </ProDescriptions>
)} )}
......
...@@ -34,5 +34,5 @@ const StudioPreview = (props:any) => { ...@@ -34,5 +34,5 @@ const StudioPreview = (props:any) => {
export default connect(({ Studio }: { Studio: StateType }) => ({ export default connect(({ Studio }: { Studio: StateType }) => ({
current: Studio.current, current: Studio.current,
result: Studio.result, // result: Studio.result,
}))(StudioPreview); }))(StudioPreview);
...@@ -32,8 +32,8 @@ const StudioTable = (props:any) => { ...@@ -32,8 +32,8 @@ const StudioTable = (props:any) => {
{current.console.result.jobId && (<Tag color="blue" key={current.console.result.jobId}> {current.console.result.jobId && (<Tag color="blue" key={current.console.result.jobId}>
<FireOutlined /> {current.console.result.jobId} <FireOutlined /> {current.console.result.jobId}
</Tag>)} </Tag>)}
{result.columns? {current.console.result.result&&current.console.result.result.columns?
<DTable dataSource={result.rowData} columns={getColumns(result.columns)}/> <DTable dataSource={current.console.result.result.rowData} columns={getColumns(current.console.result.result.columns)}/>
:(<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />) :(<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />)
} }
</>) </>)
......
import {Tabs, Empty} from "antd"; import {Tabs, Empty} from "antd";
import { import {
CodeOutlined, TableOutlined, RadarChartOutlined, CalendarOutlined, FileSearchOutlined, DesktopOutlined CodeOutlined, TableOutlined, RadarChartOutlined, CalendarOutlined, FileSearchOutlined, DesktopOutlined
, FunctionOutlined, ApartmentOutlined , FunctionOutlined, ApartmentOutlined,BarChartOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import {StateType} from "@/pages/FlinkSqlStudio/model"; import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi"; import {connect} from "umi";
...@@ -13,6 +13,7 @@ import StudioFX from "./StudioFX"; ...@@ -13,6 +13,7 @@ import StudioFX from "./StudioFX";
import StudioCA from "./StudioCA"; import StudioCA from "./StudioCA";
import StudioProcess from "./StudioProcess"; import StudioProcess from "./StudioProcess";
import {Scrollbars} from 'react-custom-scrollbars'; import {Scrollbars} from 'react-custom-scrollbars';
import Chart from "@/components/Chart";
const {TabPane} = Tabs; const {TabPane} = Tabs;
...@@ -50,6 +51,19 @@ const StudioConsole = (props: any) => { ...@@ -50,6 +51,19 @@ const StudioConsole = (props: any) => {
<StudioTable/> <StudioTable/>
</Scrollbars> </Scrollbars>
</TabPane> </TabPane>
<TabPane
tab={
<span>
<BarChartOutlined />
BI
</span>
}
key="StudioChart"
>
<Scrollbars style={{height: consoleHeight}}>
<Chart height={consoleHeight} />
</Scrollbars>
</TabPane>
<TabPane <TabPane
tab={ tab={
<span> <span>
......
import {getJobData} from "@/pages/FlinkSqlStudio/service"; import {getJobData} from "@/pages/FlinkSqlStudio/service";
export function showJobData(jobId:string,dispatch:any) { export function showJobData(jobId:string,dispatch:any) {
if(!jobId){
return;
}
const res = getJobData(jobId); const res = getJobData(jobId);
res.then((result)=>{ res.then((result)=>{
dispatch&&dispatch({ dispatch&&dispatch({
......
...@@ -11,14 +11,14 @@ import Button from "antd/es/button/button"; ...@@ -11,14 +11,14 @@ import Button from "antd/es/button/button";
import Breadcrumb from "antd/es/breadcrumb/Breadcrumb"; import Breadcrumb from "antd/es/breadcrumb/Breadcrumb";
import {StateType} from "@/pages/FlinkSqlStudio/model"; import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi"; import {connect} from "umi";
import {handleAddOrUpdate, postDataArray} from "@/components/Common/crud"; import { postDataArray} from "@/components/Common/crud";
import {executeSql, explainSql, getJobPlan} from "@/pages/FlinkSqlStudio/service"; import {executeSql, getJobPlan} from "@/pages/FlinkSqlStudio/service";
import StudioHelp from "./StudioHelp"; import StudioHelp from "./StudioHelp";
import StudioGraph from "./StudioGraph"; import StudioGraph from "./StudioGraph";
import {showCluster, showTables, saveTask} from "@/components/Studio/StudioEvent/DDL"; import {showCluster, showTables, saveTask} from "@/components/Studio/StudioEvent/DDL";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import StudioExplain from "../StudioConsole/StudioExplain"; import StudioExplain from "../StudioConsole/StudioExplain";
import {DIALECT, isSql} from "@/components/Studio/conf"; import {DIALECT, isOnline, isSql} from "@/components/Studio/conf";
import { import {
ModalForm, ModalForm,
} from '@ant-design/pro-form'; } from '@ant-design/pro-form';
...@@ -37,10 +37,13 @@ const StudioMenu = (props: any) => { ...@@ -37,10 +37,13 @@ const StudioMenu = (props: any) => {
const [modalVisible, handleModalVisible] = useState<boolean>(false); const [modalVisible, handleModalVisible] = useState<boolean>(false);
const [exportModalVisible, handleExportModalVisible] = useState<boolean>(false); const [exportModalVisible, handleExportModalVisible] = useState<boolean>(false);
const [graphModalVisible, handleGraphModalVisible] = useState<boolean>(false); const [graphModalVisible, handleGraphModalVisible] = useState<boolean>(false);
const [explainData, setExplainData] = useState([]);
const [graphData, setGraphData] = useState(); const [graphData, setGraphData] = useState();
const execute = () => { const execute = () => {
if(!isSql(current.task.dialect)&&!isOnline(current.task.type)){
message.warn(`该任务执行模式为【${current.task.type}】,不支持 SQL 查询,请手动保存后使用右侧按钮——作业提交`);
return;
}
let selectsql = null; let selectsql = null;
if (current.monaco.current) { if (current.monaco.current) {
let selection = current.monaco.current.editor.getSelection(); let selection = current.monaco.current.editor.getSelection();
...@@ -75,7 +78,7 @@ const StudioMenu = (props: any) => { ...@@ -75,7 +78,7 @@ const StudioMenu = (props: any) => {
if (res.datas.success) { if (res.datas.success) {
message.success('执行成功'); message.success('执行成功');
} else { } else {
message.success('执行失败'); message.error('执行失败');
} }
let newTabs = tabs; let newTabs = tabs;
for (let i = 0; i < newTabs.panes.length; i++) { for (let i = 0; i < newTabs.panes.length; i++) {
...@@ -230,8 +233,8 @@ const StudioMenu = (props: any) => { ...@@ -230,8 +233,8 @@ const StudioMenu = (props: any) => {
const runMenu = ( const runMenu = (
<Menu> <Menu>
<Menu.Item onClick={execute}>同步执行</Menu.Item> <Menu.Item onClick={execute}>SQL 查询</Menu.Item>
<Menu.Item onClick={submit}>异步提交</Menu.Item> <Menu.Item onClick={submit}>提交作业</Menu.Item>
</Menu> </Menu>
); );
...@@ -348,7 +351,7 @@ const StudioMenu = (props: any) => { ...@@ -348,7 +351,7 @@ const StudioMenu = (props: any) => {
/> />
</Tooltip>)} </Tooltip>)}
{(!current.task.dialect||current.task.dialect === DIALECT.FLINKSQL||isSql( current.task.dialect )) &&( {(!current.task.dialect||current.task.dialect === DIALECT.FLINKSQL||isSql( current.task.dialect )) &&(
<Tooltip title="执行当前的 FlinkSql"> <Tooltip title="执行当前的 SQL">
<Button <Button
type="text" type="text"
icon={<PlayCircleTwoTone/>} icon={<PlayCircleTwoTone/>}
...@@ -357,7 +360,7 @@ const StudioMenu = (props: any) => { ...@@ -357,7 +360,7 @@ const StudioMenu = (props: any) => {
/> />
</Tooltip>)} </Tooltip>)}
{(!current.task.dialect||current.task.dialect === DIALECT.FLINKSQL||isSql( current.task.dialect )) &&(<> {(!current.task.dialect||current.task.dialect === DIALECT.FLINKSQL||isSql( current.task.dialect )) &&(<>
<Tooltip title="提交当前的作业到集群"> <Tooltip title="提交当前的作业到集群,提交前请手动保存">
<Button <Button
type="text" type="text"
icon={<RocketTwoTone/>} icon={<RocketTwoTone/>}
......
@import '~antd/es/style/themes/default.less';
.form_setting{
padding-left: 10px;
}
.form_item{
margin-bottom: 5px;
}
import {connect} from "umi";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {Form, Switch, Row, Col, Tooltip, Button, Input} from "antd";
import {InfoCircleOutlined,MinusSquareOutlined} from "@ant-design/icons";
import styles from "./index.less";
import {useEffect} from "react";
import {JarStateType} from "@/pages/Jar/model";
import {Scrollbars} from "react-custom-scrollbars";
const StudioUDFInfo = (props: any) => {
const { current, form, toolHeight} = props;
useEffect(() => {
form.setFieldsValue(current.task);
}, [current.task]);
return (
<>
<Row>
<Col span={24}>
<div style={{float: "right"}}>
<Tooltip title="最小化">
<Button
type="text"
icon={<MinusSquareOutlined/>}
/>
</Tooltip>
</div>
</Col>
</Row>
<Scrollbars style={{height: (toolHeight - 32)}}>
<Form
form={form}
layout="vertical"
className={styles.form_setting}
>
<Row>
<Col span={24}>
<Form.Item
label="类名" className={styles.form_item} name="savePointPath"
>
<Input readOnly={true} placeholder="自动识别"/>
</Form.Item>
</Col>
</Row>
</Form>
</Scrollbars>
</>
);
};
export default connect(({Studio, Jar}: { Studio: StateType, Jar: JarStateType }) => ({
current: Studio.current,
toolHeight: Studio.toolHeight,
}))(StudioUDFInfo);
...@@ -8,6 +8,7 @@ import StudioSetting from "./StudioSetting"; ...@@ -8,6 +8,7 @@ import StudioSetting from "./StudioSetting";
import StudioSavePoint from "./StudioSavePoint"; import StudioSavePoint from "./StudioSavePoint";
import StudioEnvSetting from "./StudioEnvSetting"; import StudioEnvSetting from "./StudioEnvSetting";
import StudioSqlConfig from "./StudioSqlConfig"; import StudioSqlConfig from "./StudioSqlConfig";
import StudioUDFInfo from "./StudioUDFInfo";
import {DIALECT, isSql} from "@/components/Studio/conf"; import {DIALECT, isSql} from "@/components/Studio/conf";
const { TabPane } = Tabs; const { TabPane } = Tabs;
...@@ -26,7 +27,7 @@ const StudioRightTool = (props:any) => { ...@@ -26,7 +27,7 @@ const StudioRightTool = (props:any) => {
return renderEnvContent(); return renderEnvContent();
} }
if(DIALECT.JAVA === current.task.dialect){ if(DIALECT.JAVA === current.task.dialect){
return undefined; return renderUDFContent();
} }
return renderFlinkSqlContent(); return renderFlinkSqlContent();
}; };
...@@ -47,6 +48,14 @@ const StudioRightTool = (props:any) => { ...@@ -47,6 +48,14 @@ const StudioRightTool = (props:any) => {
</>) </>)
}; };
const renderUDFContent = () => {
return (<>
<TabPane tab={<span><SettingOutlined /> UDF信息</span>} key="StudioUDFInfo" >
<StudioUDFInfo form={form}/>
</TabPane>
</>)
};
const renderFlinkSqlContent = () => { const renderFlinkSqlContent = () => {
return (<><TabPane tab={<span><SettingOutlined /> 作业配置</span>} key="StudioSetting" > return (<><TabPane tab={<span><SettingOutlined /> 作业配置</span>} key="StudioSetting" >
<StudioSetting form={form} /> <StudioSetting form={form} />
......
import {message, Tabs} from 'antd'; import {message, Tabs, Menu, Dropdown} from 'antd';
import React, {useState} from 'react'; import React, {useState} from 'react';
import {connect} from "umi"; import {connect} from 'umi';
import {StateType} from "@/pages/FlinkSqlStudio/model"; import {StateType} from '@/pages/FlinkSqlStudio/model';
import styles from './index.less'; import styles from './index.less';
import StudioEdit from '../StudioEdit'; import StudioEdit from '../StudioEdit';
import {saveTask} from "@/components/Studio/StudioEvent/DDL"; import {saveTask} from '@/components/Studio/StudioEvent/DDL';
import { DIALECT } from '../conf'; import {DIALECT} from '../conf';
const {TabPane} = Tabs; const {TabPane} = Tabs;
const EditorTabs = (props: any) => { const EditorTabs = (props: any) => {
const {tabs, dispatch, current, toolHeight,width} = props; const {tabs, dispatch, current, toolHeight, width} = props;
const onChange = (activeKey: any) => { const onChange = (activeKey: any) => {
dispatch&&dispatch({ dispatch &&
type: "Studio/saveToolHeight", dispatch({
payload: toolHeight-0.0001, type: 'Studio/saveToolHeight',
payload: toolHeight - 0.0001,
}); });
dispatch({ dispatch({
type: "Studio/changeActiveKey", type: 'Studio/changeActiveKey',
payload: activeKey, payload: activeKey,
}); });
}; };
...@@ -27,9 +28,10 @@ const EditorTabs = (props: any) => { ...@@ -27,9 +28,10 @@ const EditorTabs = (props: any) => {
if (action == 'add') { if (action == 'add') {
add(); add();
} else if (action == 'remove') { } else if (action == 'remove') {
dispatch&&dispatch({ dispatch &&
type: "Studio/saveToolHeight", dispatch({
payload: toolHeight-0.0001, type: 'Studio/saveToolHeight',
payload: toolHeight - 0.0001,
}); });
if (current.isModified) { if (current.isModified) {
saveTask(current, dispatch); saveTask(current, dispatch);
...@@ -51,7 +53,7 @@ const EditorTabs = (props: any) => { ...@@ -51,7 +53,7 @@ const EditorTabs = (props: any) => {
} }
}); });
let panes = tabs.panes; let panes = tabs.panes;
const newPanes = panes.filter(pane => pane.key.toString() != targetKey); const newPanes = panes.filter((pane) => pane.key.toString() != targetKey);
if (newPanes.length && newActiveKey.toString() === targetKey) { if (newPanes.length && newActiveKey.toString() === targetKey) {
if (lastIndex > 0) { if (lastIndex > 0) {
newActiveKey = newPanes[lastIndex].key; newActiveKey = newPanes[lastIndex].key;
...@@ -60,13 +62,49 @@ const EditorTabs = (props: any) => { ...@@ -60,13 +62,49 @@ const EditorTabs = (props: any) => {
} }
} }
dispatch({ dispatch({
type: "Studio/saveTabs", type: 'Studio/saveTabs',
payload: { payload: {
activeKey: newActiveKey, activeKey: newActiveKey,
panes: newPanes, panes: newPanes,
}, },
}); });
}; };
const handleClickMenu = (e: any, current) => {
dispatch({
type: 'Studio/closeTabs',
payload: {
deleteType: e.key,
current
},
});
};
const menu = (pane) => (
<Menu onClick={(e) => handleClickMenu(e, pane)}>
<Menu.Item key="CLOSE_OTHER">
<span>关闭其他</span>
</Menu.Item>
<Menu.Item key="CLOSE_ALL">
<span>关闭所有</span>
</Menu.Item>
</Menu>
);
const Tab = (pane: any) => (
<span>
{pane.key === 0 ? (
pane.title
) : (
<Dropdown overlay={menu(pane)} trigger={['contextMenu']}>
<span className="ant-dropdown-link">
{pane.title}
</span>
</Dropdown>
)}
</span>
);
return ( return (
<Tabs <Tabs
hideAdd hideAdd
...@@ -75,18 +113,21 @@ const EditorTabs = (props: any) => { ...@@ -75,18 +113,21 @@ const EditorTabs = (props: any) => {
onChange={onChange} onChange={onChange}
activeKey={tabs.activeKey + ''} activeKey={tabs.activeKey + ''}
onEdit={onEdit} onEdit={onEdit}
className={styles["edit-tabs"]} className={styles['edit-tabs']}
style={{height: toolHeight}} style={{height: toolHeight}}
> >
{tabs.panes.map(pane => ( {tabs.panes.map((pane) => (
<TabPane tab={pane.title} key={pane.key} closable={pane.closable}> <TabPane tab={Tab(pane)} key={pane.key} closable={pane.closable}>
<StudioEdit tabsKey={pane.key} height={(toolHeight - 32)} width={width} <StudioEdit
language={current.task.dialect===DIALECT.JAVA?'java':'sql'}/> tabsKey={pane.key}
height={toolHeight - 32}
width={width}
language={current.task.dialect === DIALECT.JAVA ? 'java' : 'sql'}
/>
</TabPane> </TabPane>
))} ))}
</Tabs> </Tabs>
);
)
}; };
export default connect(({Studio}: { Studio: StateType }) => ({ export default connect(({Studio}: { Studio: StateType }) => ({
......
...@@ -161,7 +161,8 @@ const StudioTree: React.FC<StudioTreeProps> = (props) => { ...@@ -161,7 +161,8 @@ const StudioTree: React.FC<StudioTreeProps> = (props) => {
...result.datas, ...result.datas,
}, },
console:{ console:{
result:[], result: {},
chart: {},
}, },
monaco: React.createRef(), monaco: React.createRef(),
}; };
......
...@@ -20,8 +20,14 @@ export const DIALECT = { ...@@ -20,8 +20,14 @@ export const DIALECT = {
JAVA:'Java', JAVA:'Java',
}; };
export const isSql = (type: string)=>{ export const CHART = {
switch (type){ LINE:'折线图',
BAR:'条形图',
PIE:'饼图',
};
export const isSql = (dialect: string)=>{
switch (dialect){
case DIALECT.SQL: case DIALECT.SQL:
case DIALECT.MYSQL: case DIALECT.MYSQL:
case DIALECT.ORACLE: case DIALECT.ORACLE:
...@@ -32,4 +38,16 @@ export const isSql = (type: string)=>{ ...@@ -32,4 +38,16 @@ export const isSql = (type: string)=>{
default: default:
return false; return false;
} }
};
export const isOnline = (type: string)=>{
switch (type){
case RUN_MODE.LOCAL:
case RUN_MODE.STANDALONE:
case RUN_MODE.YARN_SESSION:
case RUN_MODE.KUBERNETES_SESSION:
return true;
default:
return false;
}
} }
...@@ -96,6 +96,7 @@ export type TaskType = { ...@@ -96,6 +96,7 @@ export type TaskType = {
export type ConsoleType = { export type ConsoleType = {
result: {}; result: {};
chart: {};
} }
export type TabsItemType = { export type TabsItemType = {
...@@ -172,6 +173,7 @@ export type ModelType = { ...@@ -172,6 +173,7 @@ export type ModelType = {
saveMonaco: Reducer<StateType>; saveMonaco: Reducer<StateType>;
saveSqlMetaData: Reducer<StateType>; saveSqlMetaData: Reducer<StateType>;
saveTabs: Reducer<StateType>; saveTabs: Reducer<StateType>;
closeTabs: Reducer<StateType>;
changeActiveKey: Reducer<StateType>; changeActiveKey: Reducer<StateType>;
saveTaskData: Reducer<StateType>; saveTaskData: Reducer<StateType>;
saveSession: Reducer<StateType>; saveSession: Reducer<StateType>;
...@@ -184,6 +186,7 @@ export type ModelType = { ...@@ -184,6 +186,7 @@ export type ModelType = {
saveClusterConfiguration: Reducer<StateType>; saveClusterConfiguration: Reducer<StateType>;
saveDataBase: Reducer<StateType>; saveDataBase: Reducer<StateType>;
saveEnv: Reducer<StateType>; saveEnv: Reducer<StateType>;
saveChart: Reducer<StateType>;
}; };
}; };
...@@ -238,6 +241,7 @@ const Model: ModelType = { ...@@ -238,6 +241,7 @@ const Model: ModelType = {
}, },
console: { console: {
result: {}, result: {},
chart: {},
}, },
monaco: {}, monaco: {},
sqlMetaData: undefined, sqlMetaData: undefined,
...@@ -284,6 +288,7 @@ const Model: ModelType = { ...@@ -284,6 +288,7 @@ const Model: ModelType = {
}, },
console: { console: {
result: {}, result: {},
chart: {},
}, },
monaco: {}, monaco: {},
sqlMetaData: undefined, sqlMetaData: undefined,
...@@ -420,6 +425,31 @@ const Model: ModelType = { ...@@ -420,6 +425,31 @@ const Model: ModelType = {
}, },
}; };
}, },
closeTabs(state, {payload}) {
const {deleteType, current} = payload;
const newTabs = state.tabs;
const firstKey = newTabs.panes[0].key;
let newCurrent = newTabs.panes[0];
if (deleteType === 'CLOSE_OTHER') {
const keys = [firstKey, current.key];
newCurrent = {...current};
newTabs.activeKey = current.key;
newTabs.panes = newTabs.panes.filter(item => keys.includes(item.key));
} else {
newTabs.panes = [];
newTabs.activeKey = firstKey
}
return {
...state,
current: {
...newCurrent
},
tabs: {
...newTabs,
}
};
},
changeActiveKey(state, {payload}) { changeActiveKey(state, {payload}) {
const {tabs} = state; const {tabs} = state;
tabs.activeKey = payload; tabs.activeKey = payload;
...@@ -484,11 +514,25 @@ const Model: ModelType = { ...@@ -484,11 +514,25 @@ const Model: ModelType = {
}; };
}, },
saveResult(state, {payload}) { saveResult(state, {payload}) {
// return {
// ...state,
// result: {
// ...payload
// },
// };
let newTabs = state?.tabs;
let newCurrent = state?.current;
for (let i = 0; i < newTabs.panes.length; i++) {
if (newTabs.panes[i].key === newTabs.activeKey) {
newTabs.panes[i].console.result.result = payload;
newCurrent = newTabs.panes[i];
break;
}
}
return { return {
...state, ...state,
result: { current: newCurrent,
...payload tabs: newTabs,
},
}; };
}, },
saveCluster(state, {payload}) { saveCluster(state, {payload}) {
...@@ -516,6 +560,21 @@ const Model: ModelType = { ...@@ -516,6 +560,21 @@ const Model: ModelType = {
...state, ...state,
env: payload, env: payload,
}; };
},saveChart(state, {payload}) {
let newTabs = state?.tabs;
let newCurrent = state?.current;
for (let i = 0; i < newTabs.panes.length; i++) {
if (newTabs.panes[i].key === newTabs.activeKey) {
newTabs.panes[i].console.chart = payload;
newCurrent = newTabs.panes[i];
break;
}
}
return {
...state,
current: newCurrent,
tabs: newTabs,
};
}, },
}, },
}; };
......
...@@ -530,7 +530,7 @@ export default (): React.ReactNode => { ...@@ -530,7 +530,7 @@ export default (): React.ReactNode => {
<Link>修复 set 语法在1.11和1.12的兼容问题</Link> <Link>修复 set 语法在1.11和1.12的兼容问题</Link>
</li> </li>
<li> <li>
<Link>升级各版本 Flink 依赖至最新版本以解决核弹问题</Link> <Link>升级 各版本 Flink 依赖至最新版本以解决核弹问题</Link>
</li> </li>
<li> <li>
<Link>新增 Yarn 的 Kerboros 验证</Link> <Link>新增 Yarn 的 Kerboros 验证</Link>
...@@ -539,10 +539,22 @@ export default (): React.ReactNode => { ...@@ -539,10 +539,22 @@ export default (): React.ReactNode => {
<Link>新增 ChangLog 和 Table 的查询及自动停止实现</Link> <Link>新增 ChangLog 和 Table 的查询及自动停止实现</Link>
</li> </li>
<li> <li>
<Link>修改项目名为 Dinky 以及图标</Link> <Link>修改 项目名为 Dinky 以及图标</Link>
</li> </li>
<li> <li>
<Link>优化血缘分析图</Link> <Link>优化 血缘分析图</Link>
</li>
<li>
<Link>新增 快捷键保存、校验、美化</Link>
</li>
<li>
<Link>新增 UDF Java方言的Local模式的在线编写、调试、动态加载</Link>
</li>
<li>
<Link>新增 编辑器选项卡右键关闭其他和关闭所有</Link>
</li>
<li>
<Link>新增 BI选项卡的折线图、条形图、饼图</Link>
</li> </li>
</ul> </ul>
</Paragraph> </Paragraph>
......
...@@ -87,6 +87,7 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对 ...@@ -87,6 +87,7 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对
| | | 新增 选中片段执行 | 0.4.0 | | | | 新增 选中片段执行 | 0.4.0 |
| | | 新增 布局拖拽 | 0.4.0 | | | | 新增 布局拖拽 | 0.4.0 |
| | | 新增 SQL导出 | 0.5.0 | | | | 新增 SQL导出 | 0.5.0 |
| | | 新增 快捷键保存、校验、美化 | 0.5.0 |
| | | 支持 local 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 local 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 standalone 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 standalone 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 yarn session 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 yarn session 模式下 FlinkSQL 提交 | 0.4.0 |
...@@ -94,16 +95,19 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对 ...@@ -94,16 +95,19 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对
| | | 支持 yarn application 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 yarn application 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 kubernetes session 模式下 FlinkSQL 提交 | 0.5.0 | | | | 支持 kubernetes session 模式下 FlinkSQL 提交 | 0.5.0 |
| | | 支持 kubernetes application 模式下 FlinkSQL 提交 | 0.5.0 | | | | 支持 kubernetes application 模式下 FlinkSQL 提交 | 0.5.0 |
| | | 支持 UDF Java 方言Local模式在线编写、调试、动态加载 | 0.5.0 |
| | Flink 作业 | 支持 yarn application 模式下 Jar 提交 | 0.4.0 | | | Flink 作业 | 支持 yarn application 模式下 Jar 提交 | 0.4.0 |
| | | 支持 k8s application 模式下 Jar 提交 | 0.5.0 | | | | 支持 k8s application 模式下 Jar 提交 | 0.5.0 |
| | | 支持 作业 Cancel | 0.4.0 | | | | 支持 作业 Cancel | 0.4.0 |
| | | 支持 作业 SavePoint 的 Cancel、Stop、Trigger | 0.4.0 | | | | 支持 作业 SavePoint 的 Cancel、Stop、Trigger | 0.4.0 |
| | | 新增 作业自动从 SavePoint 恢复机制(包含最近、最早、指定一次) | 0.4.0 | | | | 新增 作业自动从 SavePoint 恢复机制(包含最近、最早、指定一次) | 0.4.0 |
| | | 新增 UDF java方言代码的开发 | 0.5.0 |
| | Flink 集群 | 支持 查看已注册集群的作业列表与运维 | 0.4.0 | | | Flink 集群 | 支持 查看已注册集群的作业列表与运维 | 0.4.0 |
| | | 新增 自动注册 Yarn 创建的集群 | 0.4.0 | | | | 新增 自动注册 Yarn 创建的集群 | 0.4.0 |
| | SQL | 新增 外部数据源的 SQL 校验 | 0.5.0 | | | SQL | 新增 外部数据源的 SQL 校验 | 0.5.0 |
| | | 新增 外部数据源的 SQL 执行与预览 | 0.5.0 | | | | 新增 外部数据源的 SQL 执行与预览 | 0.5.0 |
| | BI | 新增 折线图的渲染 | 0.5.0 |
| | | 新增 条形图图的渲染 | 0.5.0 |
| | | 新增 饼图的渲染 | 0.5.0 |
| | 元数据 | 新增 查询外部数据源的元数据信息 | 0.4.0 | | | 元数据 | 新增 查询外部数据源的元数据信息 | 0.4.0 |
| | 归档 | 新增 执行与提交历史 | 0.4.0 | | | 归档 | 新增 执行与提交历史 | 0.4.0 |
| 运维中心 | 暂无 | 暂无 | 0.4.0 | | 运维中心 | 暂无 | 暂无 | 0.4.0 |
......
...@@ -87,6 +87,7 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对 ...@@ -87,6 +87,7 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对
| | | 新增 选中片段执行 | 0.4.0 | | | | 新增 选中片段执行 | 0.4.0 |
| | | 新增 布局拖拽 | 0.4.0 | | | | 新增 布局拖拽 | 0.4.0 |
| | | 新增 SQL导出 | 0.5.0 | | | | 新增 SQL导出 | 0.5.0 |
| | | 新增 快捷键保存、校验、美化 | 0.5.0 |
| | | 支持 local 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 local 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 standalone 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 standalone 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 yarn session 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 yarn session 模式下 FlinkSQL 提交 | 0.4.0 |
...@@ -94,16 +95,19 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对 ...@@ -94,16 +95,19 @@ Dinky 通过已注册的集群配置来获取对应的 YarnClient 实例。对
| | | 支持 yarn application 模式下 FlinkSQL 提交 | 0.4.0 | | | | 支持 yarn application 模式下 FlinkSQL 提交 | 0.4.0 |
| | | 支持 kubernetes session 模式下 FlinkSQL 提交 | 0.5.0 | | | | 支持 kubernetes session 模式下 FlinkSQL 提交 | 0.5.0 |
| | | 支持 kubernetes application 模式下 FlinkSQL 提交 | 0.5.0 | | | | 支持 kubernetes application 模式下 FlinkSQL 提交 | 0.5.0 |
| | | 支持 UDF Java 方言Local模式在线编写、调试、动态加载 | 0.5.0 |
| | Flink 作业 | 支持 yarn application 模式下 Jar 提交 | 0.4.0 | | | Flink 作业 | 支持 yarn application 模式下 Jar 提交 | 0.4.0 |
| | | 支持 k8s application 模式下 Jar 提交 | 0.5.0 | | | | 支持 k8s application 模式下 Jar 提交 | 0.5.0 |
| | | 支持 作业 Cancel | 0.4.0 | | | | 支持 作业 Cancel | 0.4.0 |
| | | 支持 作业 SavePoint 的 Cancel、Stop、Trigger | 0.4.0 | | | | 支持 作业 SavePoint 的 Cancel、Stop、Trigger | 0.4.0 |
| | | 新增 作业自动从 SavePoint 恢复机制(包含最近、最早、指定一次) | 0.4.0 | | | | 新增 作业自动从 SavePoint 恢复机制(包含最近、最早、指定一次) | 0.4.0 |
| | | 新增 UDF java方言代码的开发 | 0.5.0 |
| | Flink 集群 | 支持 查看已注册集群的作业列表与运维 | 0.4.0 | | | Flink 集群 | 支持 查看已注册集群的作业列表与运维 | 0.4.0 |
| | | 新增 自动注册 Yarn 创建的集群 | 0.4.0 | | | | 新增 自动注册 Yarn 创建的集群 | 0.4.0 |
| | SQL | 新增 外部数据源的 SQL 校验 | 0.5.0 | | | SQL | 新增 外部数据源的 SQL 校验 | 0.5.0 |
| | | 新增 外部数据源的 SQL 执行与预览 | 0.5.0 | | | | 新增 外部数据源的 SQL 执行与预览 | 0.5.0 |
| | BI | 新增 折线图的渲染 | 0.5.0 |
| | | 新增 条形图图的渲染 | 0.5.0 |
| | | 新增 饼图的渲染 | 0.5.0 |
| | 元数据 | 新增 查询外部数据源的元数据信息 | 0.4.0 | | | 元数据 | 新增 查询外部数据源的元数据信息 | 0.4.0 |
| | 归档 | 新增 执行与提交历史 | 0.4.0 | | | 归档 | 新增 执行与提交历史 | 0.4.0 |
| 运维中心 | 暂无 | 暂无 | 0.4.0 | | 运维中心 | 暂无 | 暂无 | 0.4.0 |
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment