Created
December 1, 2025 11:52
-
-
Save Quackster/9229651054fb87e8b2badd0e89aac9b6 to your computer and use it in GitHub Desktop.
LambdaJsonConverter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package asm.predicate; | |
| /** | |
| * A library for converting Java lambda predicates to JSON and back using ASM bytecode analysis. | |
| * | |
| * Due to JVM limitations with dynamically generated lambda classes, this library works best with: | |
| * 1. SerializablePredicate lambdas (recommended for runtime lambda conversion) | |
| * 2. PredicateBuilder for programmatic predicate construction | |
| * 3. Static method references that can be analyzed | |
| * | |
| * For regular Predicate lambdas, use SerializablePredicate instead. | |
| */ | |
| import org.objectweb.asm.*; | |
| import org.objectweb.asm.tree.*; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.Serializable; | |
| import java.lang.invoke.SerializedLambda; | |
| import java.lang.reflect.Method; | |
| import java.util.*; | |
| import java.util.function.Predicate; | |
| public class LambdaJsonConverter { | |
| // JSON representation of a predicate expression | |
| public static class PredicateJson { | |
| // "comparison", "logical", "methodCall", "methodChain", "constant" | |
| public String type; | |
| public String operator; | |
| public Object left; | |
| public Object right; | |
| public String method; | |
| public String fieldName; | |
| public Object value; | |
| public List<MethodCall> methodChain; // For chained method calls | |
| public PredicateJson() {} | |
| public PredicateJson(String type) { | |
| this.type = type; | |
| } | |
| // Represents a single method call in a chain | |
| public static class MethodCall { | |
| public String methodName; | |
| public List<Object> arguments; | |
| public MethodCall(String methodName) { | |
| this.methodName = methodName; | |
| this.arguments = new ArrayList<>(); | |
| } | |
| public MethodCall(String methodName, List<Object> arguments) { | |
| this.methodName = methodName; | |
| this.arguments = arguments; | |
| } | |
| } | |
| } | |
| /** | |
| * Converts any predicate to JSON string using ASM bytecode analysis. | |
| * For best results, use SerializablePredicate instead of regular Predicate. | |
| */ | |
| public static <T> String toJson(Predicate<T> predicate) { | |
| try { | |
| // Check if it's a SerializablePredicate first | |
| if (predicate instanceof SerializablePredicate) { | |
| return toJsonFromSerializable((SerializablePredicate) predicate); | |
| } | |
| // Try ASM approach for non-serializable predicates | |
| PredicateJson json = analyzeLambda(predicate); | |
| return toJsonString(json); | |
| } catch (Exception e) { | |
| throw new RuntimeException("Failed to serialize lambda. " + | |
| "For runtime lambdas, please use SerializablePredicate<T> instead of Predicate<T>. " + | |
| "Example: SerializablePredicate<User> p = user -> user.getAge() > 18;", e); | |
| } | |
| } | |
| /** | |
| * Converts a serializable predicate to JSON using SerializedLambda approach | |
| */ | |
| private static <T> String toJsonFromSerializable(SerializablePredicate<T> predicate) throws Exception { | |
| Method writeReplace = predicate.getClass().getDeclaredMethod("writeReplace"); | |
| writeReplace.setAccessible(true); | |
| SerializedLambda lambda = (SerializedLambda) writeReplace.invoke(predicate); | |
| // Get the class that contains the lambda implementation | |
| String implClass = lambda.getImplClass().replace('/', '.'); | |
| Class<?> implClazz = Class.forName(implClass); | |
| // Get the method name | |
| String implMethodName = lambda.getImplMethodName(); | |
| // Load the bytecode of the implementation class | |
| byte[] bytecode = loadClassBytesFromResource(implClazz); | |
| // Parse with ASM | |
| ClassReader reader = new ClassReader(bytecode); | |
| ClassNode classNode = new ClassNode(); | |
| reader.accept(classNode, 0); | |
| // Find the implementation method | |
| MethodNode implMethod = null; | |
| for (MethodNode method : classNode.methods) { | |
| if (method.name.equals(implMethodName)) { | |
| implMethod = method; | |
| break; | |
| } | |
| } | |
| if (implMethod == null) { | |
| throw new RuntimeException("Could not find lambda implementation method: " + implMethodName); | |
| } | |
| PredicateJson json = analyzeMethodInstructions(implMethod, classNode); | |
| return toJsonString(json); | |
| } | |
| /** | |
| * Loads bytecode from a regular class file (not a lambda class) | |
| */ | |
| private static byte[] loadClassBytesFromResource(Class<?> clazz) throws IOException { | |
| String className = clazz.getName().replace('.', '/') + ".class"; | |
| ClassLoader classLoader = clazz.getClassLoader(); | |
| if (classLoader == null) { | |
| classLoader = ClassLoader.getSystemClassLoader(); | |
| } | |
| try (InputStream is = classLoader.getResourceAsStream(className)) { | |
| if (is == null) { | |
| throw new IOException("Could not find class file: " + className); | |
| } | |
| return readAllBytes(is); | |
| } | |
| } | |
| /** | |
| * Converts a serializable predicate to JSON string | |
| */ | |
| public static <T> String toJson(SerializablePredicate<T> predicate) { | |
| return toJson((Predicate<T>) predicate); | |
| } | |
| /** | |
| * Analyzes lambda bytecode using ASM to extract the predicate structure | |
| */ | |
| private static <T> PredicateJson analyzeLambda(Predicate<T> predicate) throws Exception { | |
| if (!(predicate instanceof Serializable serializable)) { | |
| throw new IllegalArgumentException("Predicate must be Serializable"); | |
| } | |
| SerializedLambda sl = extractSerializedLambda(serializable); | |
| String implClassInternalName = sl.getImplClass(); | |
| String implMethodName = sl.getImplMethodName(); | |
| String implMethodDesc = sl.getImplMethodSignature(); // important: method type, not signature | |
| Pair<ClassNode, MethodNode> targetMethod = findMethod(implClassInternalName, implMethodName, implMethodDesc); | |
| if (targetMethod.value1() == null || targetMethod.value2() == null) { | |
| throw new RuntimeException("Could not find lambda implementation method"); | |
| } | |
| return analyzeMethodInstructions(targetMethod.value2(), targetMethod.value1()); | |
| } | |
| static SerializedLambda extractSerializedLambda(Serializable lambda) { | |
| try { | |
| Method writeReplace = lambda.getClass().getDeclaredMethod("writeReplace"); | |
| writeReplace.setAccessible(true); | |
| Object replacement = writeReplace.invoke(lambda); | |
| if (replacement instanceof SerializedLambda sl) { | |
| return sl; | |
| } | |
| throw new IllegalStateException("Not a SerializedLambda: " + replacement); | |
| } catch (ReflectiveOperationException e) { | |
| throw new RuntimeException("Unable to extract SerializedLambda", e); | |
| } | |
| } | |
| private static Pair<ClassNode, MethodNode> findMethod(String internalName, String methodName, String methodDesc) { | |
| String resource = internalName + ".class"; | |
| ClassLoader cl = Thread.currentThread().getContextClassLoader(); | |
| if (cl == null) { | |
| cl = LambdaJsonConverter.class.getClassLoader(); | |
| } | |
| try (InputStream in = cl.getResourceAsStream(resource)) { | |
| if (in == null) { | |
| throw new IllegalStateException("Cannot find class bytes: " + resource); | |
| } | |
| ClassReader cr = new ClassReader(in); | |
| ClassNode cn = new ClassNode(); | |
| cr.accept(cn, 0); | |
| for (MethodNode mn : cn.methods) { | |
| if (mn.name.equals(methodName) && mn.desc.equals(methodDesc)) { | |
| return new Pair<>(cn, mn); | |
| } | |
| } | |
| throw new IllegalStateException("Method " + methodName + methodDesc + " not found in " + internalName); | |
| } catch (IOException e) { | |
| throw new RuntimeException("Failed to read class bytes for " + internalName, e); | |
| } | |
| } | |
| /** | |
| * Gets bytecode from the class loader using ASM's built-in mechanism | |
| */ | |
| private static byte[] getBytecodeFromClassLoader(Class<?> clazz) throws IOException { | |
| try { | |
| String className = clazz.getName().replace('.', '/'); | |
| ClassLoader classLoader = clazz.getClassLoader(); | |
| if (classLoader == null) { | |
| classLoader = ClassLoader.getSystemClassLoader(); | |
| } | |
| try (InputStream is = classLoader.getResourceAsStream(className + ".class")) { | |
| if (is != null) { | |
| return readAllBytes(is); | |
| } | |
| } catch (Exception e) { | |
| // Ignore, will try reflection approach | |
| } | |
| try { | |
| return getBytecodeViaReflection(clazz); | |
| } catch (Exception e) { | |
| throw new IOException("Could not load bytecode for lambda class: " + clazz.getName() + | |
| ". Lambda classes are dynamically generated and may require special handling.", e); | |
| } | |
| } catch (Exception e) { | |
| throw new IOException("Failed to load class bytecode", e); | |
| } | |
| } | |
| /** | |
| * Attempts to get bytecode using reflection on internal JVM structures | |
| */ | |
| private static byte[] getBytecodeViaReflection(Class<?> clazz) throws Exception { | |
| try { | |
| ClassReader reader = new ClassReader(clazz.getName()); | |
| ClassWriter writer = new ClassWriter(reader, 0); | |
| reader.accept(writer, 0); | |
| return writer.toByteArray(); | |
| } catch (IOException e) { | |
| throw new Exception("Cannot access lambda bytecode directly. " + | |
| "Lambda must be created from a defined location or use SerializablePredicate.", e); | |
| } | |
| } | |
| private static byte[] readAllBytes(InputStream is) throws IOException { | |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
| byte[] buffer = new byte[4096]; | |
| int bytesRead; | |
| while ((bytesRead = is.read(buffer)) != -1) { | |
| baos.write(buffer, 0, bytesRead); | |
| } | |
| return baos.toByteArray(); | |
| } | |
| // ========================= | |
| // Enhanced method analysis | |
| // ========================= | |
| /** | |
| * Enhanced method instruction analysis to detect method chains and logical/comparison structure. | |
| */ | |
| private static PredicateJson analyzeMethodInstructions(MethodNode method, ClassNode classNode) { | |
| InsnList instructions = method.instructions; | |
| Stack<Object> stack = new Stack<>(); | |
| List<PredicateJson> conditions = new ArrayList<>(); | |
| String logicalOp = null; | |
| for (int i = 0; i < instructions.size(); i++) { | |
| AbstractInsnNode insn = instructions.get(i); | |
| switch (insn.getOpcode()) { | |
| case Opcodes.ALOAD: | |
| stack.push("ref"); | |
| break; | |
| case Opcodes.INVOKEVIRTUAL: | |
| case Opcodes.INVOKEINTERFACE: { | |
| MethodInsnNode methodInsn = (MethodInsnNode) insn; | |
| // Collect arguments from stack | |
| List<Object> args = new ArrayList<>(); | |
| int argCount = countMethodArguments(methodInsn.desc); | |
| for (int j = 0; j < argCount; j++) { | |
| if (!stack.isEmpty()) { | |
| args.add(0, stack.pop()); // Insert at beginning to maintain order | |
| } | |
| } | |
| // Check if this is part of a method chain | |
| Object target = stack.isEmpty() ? null : stack.pop(); | |
| if (target instanceof PredicateJson && | |
| ("methodCall".equals(((PredicateJson) target).type) || | |
| "methodChain".equals(((PredicateJson) target).type))) { | |
| // This is a chained call | |
| PredicateJson chain = (PredicateJson) target; | |
| if (chain.methodChain == null) { | |
| // Convert single method call to chain | |
| chain.type = "methodChain"; | |
| chain.methodChain = new ArrayList<>(); | |
| PredicateJson.MethodCall firstCall = new PredicateJson.MethodCall(chain.method); | |
| chain.methodChain.add(firstCall); | |
| chain.method = null; | |
| } | |
| // Add the new method to the chain | |
| chain.methodChain.add(new PredicateJson.MethodCall(methodInsn.name, args)); | |
| stack.push(chain); | |
| } else { | |
| // New method call (possibly the start of a chain) | |
| PredicateJson methodCall = new PredicateJson("methodCall"); | |
| methodCall.method = methodInsn.name; | |
| if (!args.isEmpty()) { | |
| // Store arguments for methods like replace("a", "b") | |
| methodCall.methodChain = new ArrayList<>(); | |
| methodCall.methodChain.add(new PredicateJson.MethodCall(methodInsn.name, args)); | |
| } | |
| stack.push(methodCall); | |
| } | |
| break; | |
| } | |
| case Opcodes.LDC: { | |
| LdcInsnNode ldcInsn = (LdcInsnNode) insn; | |
| stack.push(ldcInsn.cst); | |
| break; | |
| } | |
| case Opcodes.BIPUSH: | |
| case Opcodes.SIPUSH: { | |
| IntInsnNode intInsn = (IntInsnNode) insn; | |
| stack.push(intInsn.operand); | |
| break; | |
| } | |
| case Opcodes.ICONST_0: | |
| case Opcodes.ICONST_1: | |
| case Opcodes.ICONST_2: | |
| case Opcodes.ICONST_3: | |
| case Opcodes.ICONST_4: | |
| case Opcodes.ICONST_5: | |
| stack.push(insn.getOpcode() - Opcodes.ICONST_0); | |
| break; | |
| case Opcodes.IF_ICMPGT: | |
| case Opcodes.IF_ICMPLE: | |
| case Opcodes.IF_ICMPLT: | |
| case Opcodes.IF_ICMPGE: | |
| case Opcodes.IF_ICMPEQ: | |
| case Opcodes.IF_ICMPNE: | |
| if (stack.size() >= 2) { | |
| PredicateJson comp = new PredicateJson("comparison"); | |
| Object right = stack.pop(); | |
| Object left = stack.pop(); | |
| comp.left = convertToPredicateJson(left); | |
| comp.right = convertToPredicateJson(right); | |
| comp.operator = getOperatorFromOpcode(insn.getOpcode()); | |
| conditions.add(comp); | |
| JumpInsnNode jumpInsn = (JumpInsnNode) insn; | |
| if (logicalOp == null) { | |
| logicalOp = detectLogicalOperator(jumpInsn, i, instructions); | |
| } | |
| } | |
| break; | |
| case Opcodes.IFEQ: | |
| case Opcodes.IFNE: | |
| if (!stack.isEmpty()) { | |
| Object top = stack.pop(); | |
| if (top instanceof PredicateJson) { | |
| PredicateJson pred = (PredicateJson) top; | |
| // This is a boolean-returning method (possibly chained) | |
| if (isBooleanMethod(pred)) { | |
| conditions.add(pred); | |
| JumpInsnNode jumpInsn = (JumpInsnNode) insn; | |
| if (logicalOp == null) { | |
| logicalOp = detectLogicalOperator(jumpInsn, i, instructions); | |
| } | |
| } | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| // Combine conditions if we found multiple | |
| if (conditions.size() > 1) { | |
| PredicateJson result = conditions.get(0); | |
| for (int i = 1; i < conditions.size(); i++) { | |
| PredicateJson logical = new PredicateJson("logical"); | |
| logical.operator = logicalOp != null ? logicalOp : "&&"; | |
| logical.left = result; | |
| logical.right = conditions.get(i); | |
| result = logical; | |
| } | |
| return result; | |
| } else if (conditions.size() == 1) { | |
| return conditions.get(0); | |
| } | |
| return !stack.isEmpty() && stack.peek() instanceof PredicateJson | |
| ? (PredicateJson) stack.peek() | |
| : new PredicateJson("constant"); | |
| } | |
| private static PredicateJson convertToPredicateJson(Object obj) { | |
| if (obj instanceof PredicateJson) { | |
| return (PredicateJson) obj; | |
| } else { | |
| PredicateJson constant = new PredicateJson("constant"); | |
| constant.value = obj; | |
| return constant; | |
| } | |
| } | |
| // Helper method to count method arguments from descriptor | |
| private static int countMethodArguments(String descriptor) { | |
| int count = 0; | |
| int i = 0; | |
| // Skip until '(' | |
| while (i < descriptor.length() && descriptor.charAt(i) != '(') { | |
| i++; | |
| } | |
| if (i == descriptor.length()) { | |
| return 0; | |
| } | |
| i++; // past '(' | |
| while (i < descriptor.length() && descriptor.charAt(i) != ')') { | |
| char c = descriptor.charAt(i); | |
| if (c == 'L') { | |
| // Object type: L...; | |
| count++; | |
| i++; | |
| while (i < descriptor.length() && descriptor.charAt(i) != ';') { | |
| i++; | |
| } | |
| if (i < descriptor.length()) { | |
| i++; // skip ';' | |
| } | |
| } else if (c == '[') { | |
| // Array type: consume all '[' then the element type | |
| i++; | |
| // Next char will be type or 'L' | |
| } else { | |
| // Primitive or other single-char type | |
| if ("ZBCSIJFD".indexOf(c) >= 0) { | |
| count++; | |
| } | |
| i++; | |
| } | |
| } | |
| return count; | |
| } | |
| // Enhanced boolean method check for PredicateJson | |
| private static boolean isBooleanMethod(PredicateJson pred) { | |
| if (pred == null) return false; | |
| if ("methodChain".equals(pred.type) && pred.methodChain != null && !pred.methodChain.isEmpty()) { | |
| // Check the last method in the chain | |
| String lastMethod = pred.methodChain.get(pred.methodChain.size() - 1).methodName; | |
| return isBooleanMethodName(lastMethod); | |
| } else if ("methodCall".equals(pred.type)) { | |
| return isBooleanMethodName(pred.method); | |
| } | |
| return false; | |
| } | |
| private static boolean isBooleanMethodName(String methodName) { | |
| return methodName != null && (methodName.startsWith("is") || | |
| methodName.startsWith("has") || | |
| methodName.equals("startsWith") || | |
| methodName.equals("endsWith") || | |
| methodName.equals("contains") || | |
| methodName.equals("equals") || | |
| methodName.equals("matches")); | |
| } | |
| private static String detectLogicalOperator(JumpInsnNode jumpInsn, int currentIndex, InsnList instructions) { | |
| // Simple heuristic: if jump target is close, likely AND (short-circuit) | |
| // if jump target is far (to return), likely OR | |
| int targetIndex = instructions.indexOf(jumpInsn.label); | |
| int distance = targetIndex - currentIndex; | |
| // This is a simplified heuristic - proper detection requires more analysis | |
| return distance < 10 ? "&&" : "||"; | |
| } | |
| private static String getOperatorFromOpcode(int opcode) { | |
| switch (opcode) { | |
| case Opcodes.IF_ICMPGT: | |
| case Opcodes.IFGT: | |
| return ">"; | |
| case Opcodes.IF_ICMPLT: | |
| case Opcodes.IFLT: | |
| return "<"; | |
| case Opcodes.IF_ICMPGE: | |
| case Opcodes.IFGE: | |
| return ">="; | |
| case Opcodes.IF_ICMPLE: | |
| case Opcodes.IFLE: | |
| return "<="; | |
| case Opcodes.IF_ICMPEQ: | |
| case Opcodes.IFEQ: | |
| return "=="; | |
| case Opcodes.IF_ICMPNE: | |
| case Opcodes.IFNE: | |
| return "!="; | |
| default: | |
| return "=="; | |
| } | |
| } | |
| // ========================= | |
| // JSON -> Predicate | |
| // ========================= | |
| /** | |
| * Converts a JSON string back to a working predicate | |
| */ | |
| @SuppressWarnings("unchecked") | |
| public static <T> Predicate<T> fromJson(String json, Class<T> clazz) { | |
| PredicateJson predicateJson = fromJsonString(json); | |
| return buildPredicate(predicateJson, clazz); | |
| } | |
| private static <T> Predicate<T> buildPredicate(PredicateJson json, Class<T> clazz) { | |
| return obj -> evaluateExpression(json, obj); | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static <T> boolean evaluateExpression(PredicateJson json, T obj) { | |
| try { | |
| if (json == null) { | |
| return false; | |
| } | |
| switch (json.type) { | |
| case "comparison": { | |
| Object leftVal = resolveValue(json.left, obj); | |
| Object rightVal = resolveValue(json.right, obj); | |
| return compare(leftVal, rightVal, json.operator); | |
| } | |
| case "logical": { | |
| boolean leftResult = evaluateExpression((PredicateJson) json.left, obj); | |
| if ("&&".equals(json.operator)) { | |
| return leftResult && evaluateExpression((PredicateJson) json.right, obj); | |
| } else { | |
| return leftResult || evaluateExpression((PredicateJson) json.right, obj); | |
| } | |
| } | |
| case "methodCall": | |
| case "methodChain": { | |
| Object res = resolveValue(json, obj); | |
| if (res instanceof Boolean) { | |
| return (Boolean) res; | |
| } | |
| return false; | |
| } | |
| case "methodCallDeprecated": // not used anymore, but kept for safety | |
| return evaluateMethodCall(json, obj); | |
| case "constant": | |
| if (json.value instanceof Boolean) { | |
| return (Boolean) json.value; | |
| } | |
| return false; | |
| default: | |
| return false; | |
| } | |
| } catch (Exception e) { | |
| throw new RuntimeException("Failed to evaluate expression", e); | |
| } | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static <T> Object resolveValue(Object value, T obj) throws Exception { | |
| if (value instanceof PredicateJson) { | |
| PredicateJson json = (PredicateJson) value; | |
| if ("methodChain".equals(json.type)) { | |
| return evaluateMethodChain(json, obj); | |
| } else if ("methodCall".equals(json.type)) { | |
| if (json.methodChain != null && !json.methodChain.isEmpty()) { | |
| return evaluateMethodChain(json, obj); | |
| } else { | |
| Method method = obj.getClass().getMethod(json.method); | |
| return method.invoke(obj); | |
| } | |
| } else if ("constant".equals(json.type)) { | |
| return json.value; | |
| } | |
| } | |
| return value; | |
| } | |
| private static boolean evaluateMethodCall(PredicateJson json, Object obj) throws Exception { | |
| Method method = obj.getClass().getMethod(json.method); | |
| Object result = method.invoke(obj); | |
| if (result instanceof Boolean) { | |
| return (Boolean) result; | |
| } | |
| return false; | |
| } | |
| // Evaluate a chain of method calls | |
| @SuppressWarnings("unchecked") | |
| private static <T> Object evaluateMethodChain(PredicateJson json, T obj) throws Exception { | |
| Object current = obj; | |
| for (PredicateJson.MethodCall methodCall : json.methodChain) { | |
| // Convert arguments if needed | |
| Object[] args = new Object[methodCall.arguments.size()]; | |
| for (int i = 0; i < methodCall.arguments.size(); i++) { | |
| Object arg = methodCall.arguments.get(i); | |
| if (arg instanceof PredicateJson) { | |
| args[i] = resolveValue(arg, obj); | |
| } else { | |
| args[i] = arg; | |
| } | |
| } | |
| // Find and invoke the method | |
| Method method = findMethod(current.getClass(), methodCall.methodName, args); | |
| current = method.invoke(current, args); | |
| } | |
| return current; | |
| } | |
| // Find method by name and argument types | |
| private static Method findMethod(Class<?> clazz, String methodName, Object[] args) throws NoSuchMethodException { | |
| // Try exact match first | |
| for (Method method : clazz.getMethods()) { | |
| if (method.getName().equals(methodName) && method.getParameterCount() == args.length) { | |
| boolean matches = true; | |
| Class<?>[] paramTypes = method.getParameterTypes(); | |
| for (int i = 0; i < args.length; i++) { | |
| if (args[i] != null && | |
| !paramTypes[i].isAssignableFrom(args[i].getClass()) && | |
| !isCompatiblePrimitive(paramTypes[i], args[i].getClass())) { | |
| matches = false; | |
| break; | |
| } | |
| } | |
| if (matches) { | |
| return method; | |
| } | |
| } | |
| } | |
| // Fallback: try to get method with no args if args array is empty | |
| if (args.length == 0) { | |
| return clazz.getMethod(methodName); | |
| } | |
| throw new NoSuchMethodException("Method " + methodName + " not found in " + clazz.getName()); | |
| } | |
| private static boolean isCompatiblePrimitive(Class<?> paramType, Class<?> argType) { | |
| if (paramType == int.class) return argType == Integer.class; | |
| if (paramType == long.class) return argType == Long.class; | |
| if (paramType == double.class) return argType == Double.class; | |
| if (paramType == float.class) return argType == Float.class; | |
| if (paramType == boolean.class) return argType == Boolean.class; | |
| if (paramType == char.class) return argType == Character.class; | |
| if (paramType == byte.class) return argType == Byte.class; | |
| if (paramType == short.class) return argType == Short.class; | |
| return false; | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static boolean compare(Object left, Object right, String operator) { | |
| switch (operator) { | |
| case ">": | |
| return ((Comparable) left).compareTo(right) > 0; | |
| case "<": | |
| return ((Comparable) left).compareTo(right) < 0; | |
| case ">=": | |
| return ((Comparable) left).compareTo(right) >= 0; | |
| case "<=": | |
| return ((Comparable) left).compareTo(right) <= 0; | |
| case "==": | |
| return Objects.equals(left, right); | |
| case "!=": | |
| return !Objects.equals(left, right); | |
| case "startsWith": | |
| return left.toString().startsWith(right.toString()); | |
| case "endsWith": | |
| return left.toString().endsWith(right.toString()); | |
| case "contains": | |
| return left.toString().contains(right.toString()); | |
| default: | |
| return false; | |
| } | |
| } | |
| // ========================= | |
| // JSON (de)serialization | |
| // ========================= | |
| // Enhanced JSON serialization to handle method chains | |
| private static String toJsonString(PredicateJson json) { | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append("{"); | |
| sb.append("\"type\":\"").append(json.type).append("\""); | |
| if (json.operator != null) { | |
| sb.append(",\"operator\":\"").append(json.operator).append("\""); | |
| } | |
| if (json.method != null) { | |
| sb.append(",\"method\":\"").append(escapeJson(json.method)).append("\""); | |
| } | |
| if (json.methodChain != null && !json.methodChain.isEmpty()) { | |
| sb.append(",\"methodChain\":["); | |
| for (int i = 0; i < json.methodChain.size(); i++) { | |
| if (i > 0) sb.append(","); | |
| PredicateJson.MethodCall mc = json.methodChain.get(i); | |
| sb.append("{\"methodName\":\"").append(escapeJson(mc.methodName)).append("\""); | |
| if (!mc.arguments.isEmpty()) { | |
| sb.append(",\"arguments\":["); | |
| for (int j = 0; j < mc.arguments.size(); j++) { | |
| if (j > 0) sb.append(","); | |
| Object arg = mc.arguments.get(j); | |
| if (arg instanceof PredicateJson) { | |
| sb.append(toJsonString((PredicateJson) arg)); | |
| } else { | |
| sb.append(toJsonValue(arg)); | |
| } | |
| } | |
| sb.append("]"); | |
| } | |
| sb.append("}"); | |
| } | |
| sb.append("]"); | |
| } | |
| if (json.value != null) { | |
| sb.append(",\"value\":").append(toJsonValue(json.value)); | |
| } | |
| if (json.left != null) { | |
| sb.append(",\"left\":"); | |
| if (json.left instanceof PredicateJson) { | |
| sb.append(toJsonString((PredicateJson) json.left)); | |
| } else { | |
| sb.append(toJsonValue(json.left)); | |
| } | |
| } | |
| if (json.right != null) { | |
| sb.append(",\"right\":"); | |
| if (json.right instanceof PredicateJson) { | |
| sb.append(toJsonString((PredicateJson) json.right)); | |
| } else { | |
| sb.append(toJsonValue(json.right)); | |
| } | |
| } | |
| sb.append("}"); | |
| return sb.toString(); | |
| } | |
| private static String toJsonValue(Object value) { | |
| if (value instanceof PredicateJson) { | |
| return toJsonString((PredicateJson) value); | |
| } | |
| if (value instanceof String) { | |
| return "\"" + escapeJson((String) value) + "\""; | |
| } | |
| return String.valueOf(value); | |
| } | |
| private static String escapeJson(String str) { | |
| return str.replace("\\", "\\\\") | |
| .replace("\"", "\\\"") | |
| .replace("\n", "\\n") | |
| .replace("\r", "\\r") | |
| .replace("\t", "\\t"); | |
| } | |
| private static PredicateJson fromJsonString(String json) { | |
| json = json.trim(); | |
| if (json.startsWith("{") && json.endsWith("}")) { | |
| json = json.substring(1, json.length() - 1); | |
| } | |
| PredicateJson result = new PredicateJson(); | |
| String[] pairs = splitJsonPairs(json); | |
| for (String pair : pairs) { | |
| pair = pair.trim(); | |
| if (pair.isEmpty()) continue; | |
| int colonIndex = pair.indexOf(':'); | |
| if (colonIndex == -1) continue; | |
| String key = pair.substring(0, colonIndex).trim().replace("\"", ""); | |
| String value = pair.substring(colonIndex + 1).trim(); | |
| switch (key) { | |
| case "type": | |
| result.type = value.replace("\"", ""); | |
| break; | |
| case "operator": | |
| result.operator = value.replace("\"", ""); | |
| break; | |
| case "method": | |
| result.method = value.replace("\"", ""); | |
| break; | |
| case "value": | |
| result.value = parseJsonValue(value); | |
| break; | |
| case "left": | |
| result.left = parseJsonValue(value); | |
| break; | |
| case "right": | |
| result.right = parseJsonValue(value); | |
| break; | |
| case "methodChain": | |
| result.methodChain = parseMethodChainArray(value); | |
| break; | |
| } | |
| } | |
| return result; | |
| } | |
| private static String[] splitJsonPairs(String json) { | |
| List<String> pairs = new ArrayList<>(); | |
| int braceCount = 0; | |
| int bracketCount = 0; | |
| int start = 0; | |
| boolean inString = false; | |
| for (int i = 0; i < json.length(); i++) { | |
| char c = json.charAt(i); | |
| if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) { | |
| inString = !inString; | |
| } | |
| if (!inString) { | |
| if (c == '{') braceCount++; | |
| else if (c == '}') braceCount--; | |
| else if (c == '[') bracketCount++; | |
| else if (c == ']') bracketCount--; | |
| else if (c == ',' && braceCount == 0 && bracketCount == 0) { | |
| pairs.add(json.substring(start, i)); | |
| start = i + 1; | |
| } | |
| } | |
| } | |
| if (start < json.length()) { | |
| pairs.add(json.substring(start)); | |
| } | |
| return pairs.toArray(new String[0]); | |
| } | |
| private static List<String> splitJsonElements(String json) { | |
| List<String> elements = new ArrayList<>(); | |
| int braceCount = 0; | |
| int bracketCount = 0; | |
| int start = 0; | |
| boolean inString = false; | |
| for (int i = 0; i < json.length(); i++) { | |
| char c = json.charAt(i); | |
| if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) { | |
| inString = !inString; | |
| } | |
| if (!inString) { | |
| if (c == '{') braceCount++; | |
| else if (c == '}') braceCount--; | |
| else if (c == '[') bracketCount++; | |
| else if (c == ']') bracketCount--; | |
| else if (c == ',' && braceCount == 0 && bracketCount == 0) { | |
| elements.add(json.substring(start, i)); | |
| start = i + 1; | |
| } | |
| } | |
| } | |
| if (start < json.length()) { | |
| elements.add(json.substring(start)); | |
| } | |
| return elements; | |
| } | |
| private static Object parseJsonValue(String value) { | |
| value = value.trim(); | |
| if (value.startsWith("{")) { | |
| return fromJsonString(value); | |
| } else if (value.startsWith("[")) { | |
| return parseJsonArray(value); | |
| } else if (value.startsWith("\"")) { | |
| return value.substring(1, value.length() - 1) | |
| .replace("\\\"", "\"") | |
| .replace("\\n", "\n") | |
| .replace("\\r", "\r") | |
| .replace("\\t", "\t") | |
| .replace("\\\\", "\\"); | |
| } else if (value.equals("true")) { | |
| return true; | |
| } else if (value.equals("false")) { | |
| return false; | |
| } else { | |
| try { | |
| if (value.contains(".")) { | |
| return Double.parseDouble(value); | |
| } | |
| return Integer.parseInt(value); | |
| } catch (NumberFormatException e) { | |
| return value; | |
| } | |
| } | |
| } | |
| private static List<Object> parseJsonArray(String value) { | |
| value = value.trim(); | |
| List<Object> list = new ArrayList<>(); | |
| if (!value.startsWith("[") || !value.endsWith("]")) { | |
| return list; | |
| } | |
| String content = value.substring(1, value.length() - 1).trim(); | |
| if (content.isEmpty()) { | |
| return list; | |
| } | |
| List<String> elements = splitJsonElements(content); | |
| for (String elem : elements) { | |
| list.add(parseJsonValue(elem)); | |
| } | |
| return list; | |
| } | |
| private static List<PredicateJson.MethodCall> parseMethodChainArray(String value) { | |
| value = value.trim(); | |
| List<PredicateJson.MethodCall> chain = new ArrayList<>(); | |
| if (!value.startsWith("[") || !value.endsWith("]")) { | |
| return chain; | |
| } | |
| String content = value.substring(1, value.length() - 1).trim(); | |
| if (content.isEmpty()) { | |
| return chain; | |
| } | |
| List<String> elements = splitJsonElements(content); | |
| for (String elem : elements) { | |
| PredicateJson.MethodCall mc = parseMethodCallObject(elem.trim()); | |
| if (mc != null) { | |
| chain.add(mc); | |
| } | |
| } | |
| return chain; | |
| } | |
| private static PredicateJson.MethodCall parseMethodCallObject(String value) { | |
| value = value.trim(); | |
| if (value.startsWith("{") && value.endsWith("}")) { | |
| value = value.substring(1, value.length() - 1); | |
| } | |
| String methodName = null; | |
| List<Object> args = new ArrayList<>(); | |
| String[] pairs = splitJsonPairs(value); | |
| for (String pair : pairs) { | |
| pair = pair.trim(); | |
| if (pair.isEmpty()) continue; | |
| int colonIndex = pair.indexOf(':'); | |
| if (colonIndex == -1) continue; | |
| String key = pair.substring(0, colonIndex).trim().replace("\"", ""); | |
| String val = pair.substring(colonIndex + 1).trim(); | |
| if ("methodName".equals(key)) { | |
| methodName = val.substring(1, val.length() - 1) | |
| .replace("\\\"", "\"") | |
| .replace("\\n", "\n") | |
| .replace("\\r", "\r") | |
| .replace("\\t", "\t") | |
| .replace("\\\\", "\\"); | |
| } else if ("arguments".equals(key)) { | |
| args = parseJsonArray(val); | |
| } | |
| } | |
| if (methodName == null) { | |
| return null; | |
| } | |
| return new PredicateJson.MethodCall(methodName, args); | |
| } | |
| /** | |
| * Serializable predicate interface for lambda conversion | |
| */ | |
| @FunctionalInterface | |
| public interface SerializablePredicate<T> extends Predicate<T>, Serializable {} | |
| /** | |
| * Simple Pair implementation used internally | |
| */ | |
| public static class Pair<A, B> { | |
| private final A v1; | |
| private final B v2; | |
| public Pair(A v1, B v2) { | |
| this.v1 = v1; | |
| this.v2 = v2; | |
| } | |
| public A value1() { | |
| return v1; | |
| } | |
| public B value2() { | |
| return v2; | |
| } | |
| } | |
| /** | |
| * Builder for creating predicates programmatically | |
| */ | |
| public static class PredicateBuilder<T> { | |
| private PredicateJson root; | |
| public PredicateBuilder() { | |
| this.root = new PredicateJson(); | |
| } | |
| public PredicateBuilder<T> comparison(String method, String operator, Object value) { | |
| PredicateJson comp = new PredicateJson("comparison"); | |
| comp.operator = operator; | |
| PredicateJson methodCall = new PredicateJson("methodCall"); | |
| methodCall.method = method; | |
| comp.left = methodCall; | |
| PredicateJson constant = new PredicateJson("constant"); | |
| constant.value = value; | |
| comp.right = constant; | |
| root = comp; | |
| return this; | |
| } | |
| public PredicateBuilder<T> and(PredicateBuilder<T> other) { | |
| PredicateJson logical = new PredicateJson("logical"); | |
| logical.operator = "&&"; | |
| logical.left = this.root; | |
| logical.right = other.root; | |
| this.root = logical; | |
| return this; | |
| } | |
| public PredicateBuilder<T> or(PredicateBuilder<T> other) { | |
| PredicateJson logical = new PredicateJson("logical"); | |
| logical.operator = "||"; | |
| logical.left = this.root; | |
| logical.right = other.root; | |
| this.root = logical; | |
| return this; | |
| } | |
| public String toJson() { | |
| return toJsonString(root); | |
| } | |
| public Predicate<T> build(Class<T> clazz) { | |
| return buildPredicate(root, clazz); | |
| } | |
| } | |
| // Example User class for testing | |
| public static class User { | |
| private final int age; | |
| private final String email; | |
| public User(int age, String email) { | |
| this.age = age; | |
| this.email = email; | |
| } | |
| public int getAge() { | |
| return age; | |
| } | |
| public String getEmail() { | |
| return email; | |
| } | |
| } | |
| // Example usage with method chaining | |
| public static void main(String[] args) { | |
| System.out.println("=== Test 1: PredicateBuilder (simple comparison + AND) ==="); | |
| PredicateBuilder<User> agePredicate = new PredicateBuilder<User>() | |
| .comparison("getAge", ">", 18); | |
| PredicateBuilder<User> emailPredicate = new PredicateBuilder<User>() | |
| .comparison("getEmail", "startsWith", "alex"); | |
| String jsonBuilder = agePredicate.and(emailPredicate).toJson(); | |
| System.out.println("JSON from builder: " + jsonBuilder); | |
| Predicate<User> predicateFromBuilder = LambdaJsonConverter.fromJson(jsonBuilder, User.class); | |
| User u1 = new User(25, "[email protected]"); | |
| User u2 = new User(16, "[email protected]"); | |
| User u3 = new User(25, "[email protected]"); | |
| System.out.println("User 1 (25, alex@...): " + predicateFromBuilder.test(u1)); // true | |
| System.out.println("User 2 (16, alex@...): " + predicateFromBuilder.test(u2)); // false | |
| System.out.println("User 3 (25, bob@...): " + predicateFromBuilder.test(u3)); // false | |
| System.out.println("\n=== Test 2: Method Chaining (toLowerCase().startsWith(\"alex\")) ==="); | |
| SerializablePredicate<User> chainedPredicate = user -> | |
| user.getEmail().toLowerCase().startsWith("alex"); | |
| String json1 = LambdaJsonConverter.toJson(chainedPredicate); | |
| System.out.println("Chained lambda JSON: " + json1); | |
| Predicate<User> reconstructed1 = LambdaJsonConverter.fromJson(json1, User.class); | |
| User user1 = new User(25, "[email protected]"); | |
| User user2 = new User(30, "[email protected]"); | |
| System.out.println("User 1 (ALEX@...): " + reconstructed1.test(user1)); // true | |
| System.out.println("User 2 (BOB@...): " + reconstructed1.test(user2)); // false | |
| System.out.println("\n=== Test 3: Complex chain with replace ==="); | |
| SerializablePredicate<User> complexChain = user -> | |
| user.getEmail().toLowerCase().replace("@", "_at_").startsWith("alex"); | |
| String json2 = LambdaJsonConverter.toJson(complexChain); | |
| System.out.println("Complex chain JSON: " + json2); | |
| Predicate<User> reconstructed2 = LambdaJsonConverter.fromJson(json2, User.class); | |
| System.out.println("User 1 result: " + reconstructed2.test(user1)); // true / false depending on email | |
| System.out.println("User 2 result: " + reconstructed2.test(user2)); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment