Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/main/java/com/alibaba/qlexpress4/Express4Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.alibaba.qlexpress4.runtime.context.ObjectFieldExpressContext;
import com.alibaba.qlexpress4.runtime.context.QLAliasContext;
import com.alibaba.qlexpress4.runtime.function.CustomFunction;
import com.alibaba.qlexpress4.runtime.function.ExtendFieldHandler;
import com.alibaba.qlexpress4.runtime.function.ExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.QMethodFunction;
import com.alibaba.qlexpress4.runtime.instruction.QLInstruction;
Expand Down Expand Up @@ -516,7 +517,19 @@ public boolean addCompileTimeFunction(String name, CompileTimeFunction compileTi
public void addExtendFunction(ExtensionFunction extensionFunction) {
this.reflectLoader.addExtendFunction(extensionFunction);
}


/**
* Register a custom field-access handler bound to {@code bindingClass}, used to access
* fields of non-standard containers (e.g. Flink Row, JDBC ResultSet) with the regular
* {@code obj.fieldName} syntax. Delegates to {@link ReflectLoader#addExtendFieldHandler}.
*
* @param bindingClass the receiver type the handler is bound to
* @param fieldHandler the field-access handler
*/
public void addExtendFieldHandler(Class<?> bindingClass, ExtendFieldHandler fieldHandler) {
this.reflectLoader.addExtendFieldHandler(bindingClass, fieldHandler);
}

/**
* add an extension function with variable arguments.
* @param name the name of the extension function
Expand Down
54 changes: 51 additions & 3 deletions src/main/java/com/alibaba/qlexpress4/runtime/ReflectLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.alibaba.qlexpress4.runtime.data.DataValue;
import com.alibaba.qlexpress4.runtime.data.FieldValue;
import com.alibaba.qlexpress4.runtime.data.MapItemValue;
import com.alibaba.qlexpress4.runtime.function.ExtendFieldHandler;
import com.alibaba.qlexpress4.runtime.function.ExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.FilterExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.MapExtensionFunction;
Expand Down Expand Up @@ -48,7 +49,14 @@ public class ReflectLoader {
*/
private final List<ExtensionFunction> extensionFunctions =
new CopyOnWriteArrayList<>(Arrays.asList(FilterExtensionFunction.INSTANCE, MapExtensionFunction.INSTANCE));


/**
* Custom field-access handlers registered by the user, keyed by their binding class
* (e.g. Flink Row, JDBC ResultSet or other non-standard containers). A handler is
* invoked only when the bean is assignable to its binding class.
*/
private final Map<Class<?>, ExtendFieldHandler> fieldHandlers = new ConcurrentHashMap<>();
Comment thread
Ayasaz marked this conversation as resolved.
Outdated

public ReflectLoader(QLSecurityStrategy securityStrategy, boolean allowPrivateAccess) {
this.securityStrategy = securityStrategy;
this.allowPrivateAccess = allowPrivateAccess;
Expand All @@ -57,7 +65,20 @@ public ReflectLoader(QLSecurityStrategy securityStrategy, boolean allowPrivateAc
public void addExtendFunction(ExtensionFunction extensionFunction) {
extensionFunctions.add(extensionFunction);
}


/**
* Register a custom field-access handler bound to {@code bindingClass}, used to access
* fields of non-standard containers (e.g. Flink Row, JDBC ResultSet or user-defined
* MapLike/CollectionLike) during the field-access stage of a QL expression.
* The handler is invoked only when the bean is assignable to {@code bindingClass}.
*
* @param bindingClass the receiver type the handler is bound to
* @param fieldHandler the field-access handler
*/
public void addExtendFieldHandler(Class<?> bindingClass, ExtendFieldHandler fieldHandler) {
fieldHandlers.put(bindingClass, fieldHandler);
}

public Constructor<?> loadConstructor(Class<?> cls, Class<?>[] paramTypes) {
if (securityStrategy instanceof StrategyIsolation) {
return null;
Expand All @@ -81,6 +102,12 @@ public Constructor<?> loadConstructor(Class<?> cls, Class<?>[] paramTypes) {
}

public Value loadField(Object bean, String fieldName, boolean skipSecurity, ErrorReporter errorReporter) {
// first try the user-registered custom field handlers (e.g. Flink Row, JDBC ResultSet)
Value extended = loadExtendField(bean, fieldName);
if (extended != null) {
return extended;
}

if (bean.getClass().isArray() && BasicUtil.LENGTH.equals(fieldName)) {
return new DataValue(((Object[])bean).length);
}
Expand All @@ -104,7 +131,28 @@ else if (bean instanceof MetaClass) {
return loadJavaField(bean.getClass(), bean, fieldName, skipSecurity, errorReporter);
}
}


/**
* Dispatch the field access to a user-registered handler whose binding class is assignable
* from the bean's class. Returns {@code null} when no handler matches or the matched handler
* yields {@code null}, so that the caller falls back to the default reflection logic.
*/
private Value loadExtendField(Object bean, String fieldName) {
if (fieldHandlers.isEmpty()) {
return null;
}
Class<?> beanClass = bean.getClass();
for (Map.Entry<Class<?>, ExtendFieldHandler> entry : fieldHandlers.entrySet()) {
if (entry.getKey().isAssignableFrom(beanClass)) {
Object value = entry.getValue().getField(bean, fieldName);
if (value != null) {
return new DataValue(value);
}
}
}
return null;
}

public IMethod loadMethod(Object bean, String methodName, Class<?>[] argTypes) {
boolean isStaticMethod = bean instanceof MetaClass;
Class<?> clz = isStaticMethod ? ((MetaClass)bean).getClz() : bean.getClass();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.alibaba.qlexpress4.runtime.function;

/**
* Custom field-access handler bound to a specific receiver type.
* <p>
* It extends the behaviour of {@link com.alibaba.qlexpress4.runtime.ReflectLoader#loadField}
* so that non-standard containers (such as Flink Row, JDBC ResultSet or user-defined
* MapLike/CollectionLike structures) can be accessed with the regular {@code obj.fieldName}
* syntax in QL expressions.
* <p>
* A handler is registered against a binding class via
* {@link com.alibaba.qlexpress4.Express4Runner#addExtendFieldHandler(Class, ExtendFieldHandler)}
* and is only invoked when the bean is assignable to that binding class. Binding to a class
* keeps each registration isolated (handlers cannot conflict with each other) and frees the
* caller from dealing with low-level runtime structures: just return the raw field value.
*
* <p>Example —— supporting Flink Row:
* <pre>{@code
* runner.addExtendFieldHandler(org.apache.flink.types.Row.class,
* (bean, fieldName) -> ((Row) bean).getField(fieldName));
* }</pre>
*
* <p>If the handler returns {@code null}, QLExpress falls back to the default Java reflection
* field-access logic of {@code ReflectLoader}.
*
* @author ayasaz
* @since QLExpress4
*/
@FunctionalInterface
public interface ExtendFieldHandler {

/**
* Resolve the value of {@code fieldName} from the given bean.
*
* @param bean the receiver object, guaranteed to be assignable to the binding class
* @param fieldName the field name being accessed
* @return the raw field value, or {@code null} to fall back to the default reflection logic
*/
Object getField(Object bean, String fieldName);
}
92 changes: 92 additions & 0 deletions src/test/java/com/alibaba/qlexpress4/ExtendFieldHandlerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.alibaba.qlexpress4;

import com.alibaba.qlexpress4.runtime.Value;
import org.junit.Assert;
import org.junit.Test;

/**
* Unit tests for {@link com.alibaba.qlexpress4.runtime.function.ExtendFieldHandler}.
* They verify class-bound custom field access: a matched handler resolves the value,
* a non-matching bean falls through to the default reflection logic, a handler returning
* {@code null} falls back to the default logic, and binding to a super type works for subtypes.
*
* @author ayasaz
*/
public class ExtendFieldHandlerTest {

/**
* A non-standard MapLike container (a simplified model of Flink Row / Spark Row).
* Fields are stored as String[] + Object[] and can only be read through getValue(name);
* they are not reachable through ordinary Java reflection getters.
*/
static class RowLike {
private final String[] fields;
private final Object[] values;

RowLike(String[] fields, Object[] values) {
this.fields = fields;
this.values = values;
}

Object getValue(String fieldName) {
for (int i = 0; i < fields.length; i++) {
if (fields[i].equals(fieldName)) {
return values[i];
}
}
return null;
}
}

@Test
public void testCustomFieldHandlerMatches() {
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);

runner.addExtendFieldHandler(RowLike.class, (bean, fieldName) -> ((RowLike) bean).getValue(fieldName));

RowLike row = new RowLike(new String[] { "name", "age" }, new Object[] { "张三", 30 });
Value result = runner.loadField(row, "name");
Assert.assertEquals("张三", result.get());
}

@Test
public void testCustomFieldHandlerNotMatches() {
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);

runner.addExtendFieldHandler(RowLike.class, (bean, fieldName) -> ((RowLike) bean).getValue(fieldName));

// a plain Java object that is not a RowLike should still go through the default reflection path.
// String has a getter for bytes, so it works as an ordinary Java bean here.
String hello = "hello";
Value result = runner.loadField(hello, "bytes");
Assert.assertNotNull(result);
Assert.assertTrue(result.get() instanceof byte[]);
}

@Test
public void testFieldHandlerReturnsNullFallsThrough() {
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);

// always returns null -> should fall back to the default logic
runner.addExtendFieldHandler(java.util.Map.class, (bean, fieldName) -> null);

// the default loadField has built-in support for Map (returns MapItemValue)
java.util.Map<String, Object> map = new java.util.HashMap<>();
map.put("key", "value");
Value result = runner.loadField(map, "key");
Assert.assertNotNull(result);
Assert.assertEquals("value", result.get());
}

@Test
public void testHandlerBoundToSuperTypeMatchesSubType() {
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);

// bind to the super type; a subclass instance should still be dispatched to this handler
runner.addExtendFieldHandler(RowLike.class, (bean, fieldName) -> ((RowLike) bean).getValue(fieldName));

RowLike row = new RowLike(new String[] { "city" }, new Object[] { "杭州" }) {
};
Assert.assertEquals("杭州", runner.loadField(row, "city").get());
}
}