Java tutorial
/******************************************************************************* * Copyright (c) 2015 Red Hat. All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which accompanies this * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: Red Hat - Initial Contribution *******************************************************************************/ package org.lambdamatic.analyzer; import java.io.IOException; import java.lang.invoke.SerializedLambda; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.lang3.tuple.Pair; import org.lambdamatic.analyzer.ast.CapturedArgumentsEvaluator; import org.lambdamatic.analyzer.ast.ExpressionSanitizer; import org.lambdamatic.analyzer.ast.LambdaExpressionReader; import org.lambdamatic.analyzer.ast.ReturnTruePathFilter; import org.lambdamatic.analyzer.ast.SerializedLambdaInfo; import org.lambdamatic.analyzer.ast.StatementExpressionsDelegateVisitor; import org.lambdamatic.analyzer.ast.node.NodeUtils; import org.lambdamatic.analyzer.ast.node.CapturedArgument; import org.lambdamatic.analyzer.ast.node.ControlFlowStatement; import org.lambdamatic.analyzer.ast.node.Expression; import org.lambdamatic.analyzer.ast.node.Expression.ExpressionType; import org.lambdamatic.analyzer.ast.node.ExpressionStatement; import org.lambdamatic.analyzer.ast.node.ExpressionVisitorUtil; import org.lambdamatic.analyzer.ast.node.CompoundExpression; import org.lambdamatic.analyzer.ast.node.CompoundExpression.CompoundExpressionOperator; import org.lambdamatic.analyzer.ast.node.LambdaExpression; import org.lambdamatic.analyzer.ast.node.LocalVariable; import org.lambdamatic.analyzer.ast.node.ReturnStatement; import org.lambdamatic.analyzer.ast.node.Statement; import org.lambdamatic.analyzer.ast.node.Statement.StatementType; import org.lambdamatic.analyzer.exception.AnalyzeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Singleton service that analyzes the bytecode behind a Lambda Expression and returns its AST in * the form of an {@link Expression}, or executes the actual Lambda expression and returns the * resulting Java object. * * <p> * <strong>Note:</strong>Subsequent calls to analyze a given Lambda Expression return a cached * version of the AST, only {@link CapturedArgument} may be different. * </p> * * @author Xavier Coulon * * @see <a href="http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html">Translation * of Lambda Expressions</a> * */ public class LambdaExpressionAnalyzer { /** The usual logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(LambdaExpressionAnalyzer.class); /** singleton instance. */ private static LambdaExpressionAnalyzer instance = new LambdaExpressionAnalyzer(); /** * {@link Expression} indexed by their functional implementation className.methodName. */ private final Map<String, LambdaExpression> cache = new HashMap<>(); private final Set<LambdaExpressionAnalyzerListener> listeners = new HashSet<>(); /** * Private constructor of the singleton. */ private LambdaExpressionAnalyzer() { } /** * Adds the given {@link LambdaExpressionAnalyzerListener} to the list of listeners to be notified * when a Lambda Expression is analyzed. Has no effect if the same instance is already registered. * * @param listener the listener to add. */ public void addListener(final LambdaExpressionAnalyzerListener listener) { this.listeners.add(listener); } /** * Removes the given {@link LambdaExpressionAnalyzerListener} from the list of listeners to be * notified when a Lambda Expression is analyzed. Has no effect if the same instance ss not * registered. * * @param listener the listener to remove. */ public void removeListener(final LambdaExpressionAnalyzerListener listener) { this.listeners.remove(listener); } /** * @return the singleton instance. */ public static LambdaExpressionAnalyzer getInstance() { return instance; } /** * Returns the {@link SerializedLambdaInfo} for the given {@code expression} * * @param expression the expression to analyze. * @return the corresponding {@link SerializedLambda} * @throws AnalyzeException if something wrong happened (a {@link NoSuchMethodException}, * {@link IllegalArgumentException} or {@link InvocationTargetException} exception * occurred). * * @see http ://docs.oracle.com/javase/8/docs/api/java/lang/invoke/SerializedLambda.html * @see http ://stackoverflow.com/questions/21860875/printing-debug-info-on-errors * -with-java-8-lambda-expressions/21879031 #21879031 */ private static SerializedLambdaInfo getSerializedLambdaInfo(final Object expression) { final Class<?> cl = expression.getClass(); try { final Method m = cl.getDeclaredMethod("writeReplace"); m.setAccessible(true); final Object result = m.invoke(expression); if (result instanceof SerializedLambda) { final SerializedLambda serializedLambda = (SerializedLambda) result; LOGGER.debug(" Lambda FunctionalInterface: {}.{} ({})", serializedLambda.getFunctionalInterfaceClass(), serializedLambda.getFunctionalInterfaceMethodName(), serializedLambda.getFunctionalInterfaceMethodSignature()); LOGGER.debug(" Lambda Implementation: {}.{} ({})", serializedLambda.getImplClass(), serializedLambda.getImplMethodName(), serializedLambda.getImplMethodSignature()); IntStream.range(0, serializedLambda.getCapturedArgCount()) .forEach(i -> LOGGER.debug(" with Captured Arg(" + i + "): '" + serializedLambda.getCapturedArg(i) + ((serializedLambda.getCapturedArg(i) != null) ? "' (" + serializedLambda.getCapturedArg(i).getClass().getName() + ")" : ""))); return new SerializedLambdaInfo(serializedLambda); } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new AnalyzeException("Failed to find the Serialized form for the given Lambda Expression", e); } return null; } /** * Gets the type of the argument used in the Functional Interface. * * @param expression the Lambda Expression * @return the argument type */ public static Class<?> getArgumentType(final Object expression) { final SerializedLambdaInfo lambdaInfo = getSerializedLambdaInfo(expression); return getArgumentType(lambdaInfo); } /** * Analyzes the Java Bytecode for the given user-defined Lambda Expression object (whose body has * already been desugared by the compiler into a method in the caller class) * * @param lambdaExpression the user-defined Lambda Expression to parse * @return an {@link Expression} based on the bytecode generated to execute the given * {@code lambdaExpression}. * @throws AnalyzeException if the analysis failed */ public LambdaExpression analyzeExpression(final Object lambdaExpression) throws AnalyzeException { final SerializedLambdaInfo lambdaInfo = getSerializedLambdaInfo(lambdaExpression); final LambdaExpression rawExpression = analyzeExpression(lambdaInfo); final List<Statement> result = evaluateCapturedArguments(rawExpression.getBody(), lambdaInfo.getCapturedArguments()); return new LambdaExpression(result, rawExpression.getArgumentType(), rawExpression.getArgumentName()); } /** * Analyzes the Java Bytecode for the given user-defined Lambda Expression object (whose body has * already been desugared by the compiler into a method in the caller class) * * @param serializedLambdaInfo the {@link SerializedLambdaInfo} about the user-defined Lambda * Expression to parse * @return an {@link Expression} based on the bytecode generated to execute the given * {@code lambdaExpression}. * @throws AnalyzeException if the analysis failed. */ public LambdaExpression analyzeExpression(final SerializedLambdaInfo serializedLambdaInfo) throws AnalyzeException { try { final String methodImplementationId = serializedLambdaInfo.getImplMethodId(); synchronized (methodImplementationId) { if (this.cache.containsKey(methodImplementationId)) { this.listeners.stream().forEach(l -> l.cacheHit(methodImplementationId)); } else { this.listeners.stream().forEach(l -> l.cacheMissed(methodImplementationId)); final LambdaExpression rawExpression = analyzeByteCode(serializedLambdaInfo); this.cache.put(methodImplementationId, rawExpression); } // we need to return a duplicate of the expression to be sure the original is kept // *unchanged* return (LambdaExpression) this.cache.get(methodImplementationId).duplicate(); } } catch (IOException e) { throw new AnalyzeException("Failed to analyze lambda expression", e); } } /** * Performs the actual bytecode analysis from the given {@link SerializedLambda}. * * @param serializedLambda the info about the bytecode method to analyze * @return the AST {@link Expression} * @throws IOException if a problem occurred while reading the underlying {@link Class} */ private LambdaExpression analyzeByteCode(final SerializedLambdaInfo lambdaInfo) throws IOException { LOGGER.debug("Analyzing lambda expression bytecode at {}.{}", lambdaInfo.getImplClassName(), lambdaInfo.getImplMethodName()); final LambdaExpressionReader lambdaExpressionReader = new LambdaExpressionReader(); final Pair<List<Statement>, List<LocalVariable>> bytecode = lambdaExpressionReader .readBytecodeStatement(lambdaInfo); final List<LocalVariable> lambdaExpressionArguments = bytecode.getRight(); final List<Statement> lambdaExpressionStatements = bytecode.getLeft(); final List<Statement> processedBlock = lambdaExpressionStatements.stream().map(s -> thinOut(s)) .map(s -> simplify(s)).collect(Collectors.toList()); // first argument that is not a captured argument. final LocalVariable lambdaExpressionArgument = lambdaExpressionArguments .get(lambdaInfo.getCapturedArguments().size()); return new LambdaExpression(processedBlock, lambdaExpressionArgument.getJavaType(), lambdaExpressionArgument.getName()); } private Statement simplify(final Statement statement) { switch (statement.getStatementType()) { case CONTROL_FLOW_STMT: final ControlFlowStatement controlFlowStmt = (ControlFlowStatement) statement; final Expression simplifiedControlFlowExpression = simplify(controlFlowStmt.getControlFlowExpression()); final List<Statement> simplifiedThenStmts = controlFlowStmt.getThenStatements().stream() .map(s -> simplify(s)).collect(Collectors.toList()); final List<Statement> simplifiedElseStmts = controlFlowStmt.getElseStatements().stream() .map(s -> simplify(s)).collect(Collectors.toList()); return new ControlFlowStatement(simplifiedControlFlowExpression, simplifiedThenStmts, simplifiedElseStmts); case EXPRESSION_STMT: final ExpressionStatement expressionStmt = (ExpressionStatement) statement; final Expression simplifiedExpression = simplify(expressionStmt.getExpression()); return new ExpressionStatement(simplifiedExpression); case RETURN_STMT: final ReturnStatement returnStmt = (ReturnStatement) statement; final Expression simplifiedReturnExpression = simplify(returnStmt.getExpression()); return new ReturnStatement(simplifiedReturnExpression); default: throw new AnalyzeException("Unexpected statement type to simplify: " + statement.getStatementType()); } } /** * @param expression the {@link Expression} to simplify * @return a simplified {@link Expression} if the given one is an {@link ExpressionType#COMPOUND}, * otherwise returns the given {@link Expression}. */ private static Expression simplify(final Expression expression) { if (expression.getExpressionType() == ExpressionType.COMPOUND) { final CompoundExpression infixExpression = (CompoundExpression) expression; final Expression simplifiedExpression = infixExpression.simplify(); return ExpressionVisitorUtil.visit(simplifiedExpression, new ExpressionSanitizer()); } return ExpressionVisitorUtil.visit(expression, new ExpressionSanitizer()); } /** * Performs the method calls on the {@link CapturedArgument}s wherever they would appear in the * given {@link List} of {@link Statement}. * * @param statements the {@link List} of {@link Statement} containing arguments to evaluate. * @param capturedArguments the actual captured arguments during the call. * @return the equivalent expression, where method calls on {@link CapturedArgument}s have been * replaced with their actual values. */ public static List<Statement> evaluateCapturedArguments(final List<Statement> statements, final List<CapturedArgument> capturedArguments) { // nothing to process if (capturedArguments.isEmpty()) { return statements; } else { // retrieve the captured arguments from the given serializedLambda final List<Object> capturedArgValues = capturedArguments.stream().map(a -> a.getValue()) .collect(Collectors.toList()); statements.stream().forEach(s -> s.accept( new StatementExpressionsDelegateVisitor(new CapturedArgumentsEvaluator(capturedArgValues)))); // final StatementVisitor visitor = new CapturedArgumentsEvaluator(capturedArgValues); // return ExpressionVisitorUtil.visit(sourceExpression, visitor); return statements; } } /** * Simplify the given {@link Statement} keeping all branches that end with a "return 1" node, and * combining the remaining ones in an {@link CompoundExpression}. * * @param statement the statement to thin out * @return the resulting "thined out" {@link Statement} */ private static Statement thinOut(final Statement statement) { LOGGER.debug("About to simplify \n\t{}", NodeUtils.prettyPrint(statement)); if (statement.getStatementType() == StatementType.EXPRESSION_STMT) { return statement; } else { // find branches that end with 'return 1' final ReturnTruePathFilter filter = new ReturnTruePathFilter(); statement.accept(filter); final List<ReturnStatement> returnStmts = filter.getReturnStmts(); final List<Expression> expressions = new ArrayList<>(); for (ReturnStatement returnStmt : returnStmts) { final LinkedList<Expression> relevantExpressions = new LinkedList<>(); // current node being evaluated Statement currentStmt = returnStmt; // previous node evaluated, because it is important to remember // the path that was taken (in case of ConditionalStatements) Statement previousStmt = null; while (currentStmt != null) { switch (currentStmt.getStatementType()) { case CONTROL_FLOW_STMT: final ControlFlowStatement controlFlowStatement = (ControlFlowStatement) currentStmt; final Expression controlFlowExpression = controlFlowStatement.getControlFlowExpression(); // if we come from the "eval true" path on this // condition if (controlFlowStatement.getThenStatements().contains(previousStmt)) { relevantExpressions.add(0, controlFlowExpression); } else { relevantExpressions.add(0, controlFlowExpression.inverse()); } break; case RETURN_STMT: final Expression returnExpression = ((ReturnStatement) currentStmt).getExpression(); if (returnExpression.getExpressionType() == ExpressionType.METHOD_INVOCATION) { relevantExpressions.add(0, returnExpression); } break; default: LOGGER.trace("Ignoring node '{}'", currentStmt); break; } previousStmt = currentStmt; currentStmt = currentStmt.getParent(); } if (relevantExpressions.size() > 1) { expressions.add(new CompoundExpression(CompoundExpressionOperator.CONDITIONAL_AND, relevantExpressions)); } else if (!relevantExpressions.isEmpty()) { expressions.add(relevantExpressions.getFirst()); } } if (expressions.isEmpty()) { return statement; } final Statement result = (expressions.size() > 1) ? new ReturnStatement( new CompoundExpression(CompoundExpressionOperator.CONDITIONAL_OR, expressions)) : new ReturnStatement(expressions.get(0)); LOGGER.debug("Thinned out expression: {}", result.toString()); return result; } } }