content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * Licensed 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.activiti.core.el.juel;
  
  import jakarta.el.ELContext;
  import jakarta.el.ELException;
  import jakarta.el.FunctionMapper;
  import jakarta.el.MethodExpression;
  import jakarta.el.MethodInfo;
  import jakarta.el.VariableMapper;
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.io.PrintWriter;
  import java.util.Arrays;
  import org.activiti.core.el.juel.misc.LocalMessages;
  import org.activiti.core.el.juel.misc.TypeConverter;
  import org.activiti.core.el.juel.tree.Bindings;
  import org.activiti.core.el.juel.tree.ExpressionNode;
  import org.activiti.core.el.juel.tree.NodePrinter;
  import org.activiti.core.el.juel.tree.Tree;
  import org.activiti.core.el.juel.tree.TreeBuilder;
  import org.activiti.core.el.juel.tree.TreeStore;
  
  /**
   * A method expression is ready to be evaluated (by calling either
   * {@link #invoke(ELContext, Object[])} or {@link #getMethodInfo(ELContext)}).
   *
   * Instances of this class are usually created using an {@link ExpressionFactoryImpl}.
   *
   * @author Christoph Beck
   */
  public final class TreeMethodExpression extends MethodExpression {
  
      private static final long serialVersionUID = 1L;
  
      private final TreeBuilder builder;
      private final Bindings bindings;
      private final String expr;
      private final Class<?> type;
      private final Class<?>[] types;
      private final boolean deferred;
  
      private transient ExpressionNode node;
  
      private String structure;
  
      /**
       * Create a new method expression.
       * The expression must be an lvalue expression or literal text.
       * The expected return type may be <code>null</code>, meaning "don't care".
       * If it is an lvalue expression, the parameter types must not be <code>null</code>.
       * If it is literal text, the expected return type must not be <code>void</code>.
       * @param store used to get the parse tree from.
       * @param functions the function mapper used to bind functions
       * @param variables the variable mapper used to bind variables
       * @param expr the expression string
       * @param returnType the expected return type (may be <code>null</code>)
       * @param paramTypes the expected parameter types (must not be <code>null</code> for lvalues)
       */
      public TreeMethodExpression(
          TreeStore store,
          FunctionMapper functions,
          VariableMapper variables,
          TypeConverter converter,
          String expr,
          Class<?> returnType,
          Class<?>[] paramTypes
      ) {
          super();
          Tree tree = store.get(expr);
  
          this.builder = store.getBuilder();
          this.bindings = tree.bind(functions, variables, converter);
          this.expr = expr;
          this.type = returnType;
          this.types = paramTypes;
          this.node = tree.getRoot();
          this.deferred = tree.isDeferred();
  
          if (node.isLiteralText()) {
              if (returnType == void.class || returnType == Void.class) {
                  throw new ELException(LocalMessages.get("error.method.literal.void", expr));
              }
          } else if (!node.isMethodInvocation()) {
              if (!node.isLeftValue()) {
                  throw new ELException(LocalMessages.get("error.method.invalid", expr));
              }
              if (paramTypes == null) {
                  throw new NullPointerException(LocalMessages.get("error.method.notypes")); // EL specification requires NPE
              }
          }
      }
  
      private String getStructuralId() {
          if (structure == null) {
              structure = node.getStructuralId(bindings);
          }
          return structure;
      }
  
      /**
       * Evaluates the expression and answers information about the method
       * @param context used to resolve properties (<code>base.property</code> and <code>base[property]</code>)
       * @return method information or <code>null</code> for literal expressions
       * @throws ELException if evaluation fails (e.g. suitable method not found)
       */
      @Override
      public MethodInfo getMethodInfo(ELContext context) throws ELException {
          return node.getMethodInfo(bindings, context, type, types);
      }
  
      @Override
      public String getExpressionString() {
          return expr;
      }
  
      /**
       * Evaluates the expression and invokes the method.
       * @param context used to resolve properties (<code>base.property</code> and <code>base[property]</code>)
       * @param paramValues
       * @return method result or <code>null</code> if this is a literal text expression
       * @throws ELException if evaluation fails (e.g. suitable method not found)
       */
      @Override
      public Object invoke(ELContext context, Object[] paramValues) throws ELException {
          return node.invoke(bindings, context, type, types, paramValues);
      }
  
      /**
       * @return <code>true</code> if this is a literal text expression
       */
      @Override
      public boolean isLiteralText() {
          return node.isLiteralText();
      }
  
      /**
       * @return <code>true</code> if this is a method invocation expression
       */
      @Override
      public boolean isParametersProvided() {
          return node.isMethodInvocation();
      }
  
      /**
       * Answer <code>true</code> if this is a deferred expression (starting with <code>#{</code>)
       */
      public boolean isDeferred() {
          return deferred;
      }
  
      /**
       * Expressions are compared using the concept of a <em>structural id</em>:
       * variable and function names are anonymized such that two expressions with
       * same tree structure will also have the same structural id and vice versa.
       * Two method expressions are equal if
       * <ol>
       * <li>their builders are equal</li>
       * <li>their structural id's are equal</li>
       * <li>their bindings are equal</li>
       * <li>their expected types match</li>
       * <li>their parameter types are equal</li>
       * </ol>
       */
      @Override
      public boolean equals(Object obj) {
          if (obj != null && obj.getClass() == getClass()) {
              TreeMethodExpression other = (TreeMethodExpression) obj;
              if (!builder.equals(other.builder)) {
                  return false;
              }
              if (type != other.type) {
                  return false;
              }
              if (!Arrays.equals(types, other.types)) {
                  return false;
              }
              return (getStructuralId().equals(other.getStructuralId()) && bindings.equals(other.bindings));
          }
          return false;
      }
  
      @Override
      public int hashCode() {
          return getStructuralId().hashCode();
      }
  
      @Override
      public String toString() {
          return "TreeMethodExpression(" + expr + ")";
      }
  
      /**
       * Print the parse tree.
       * @param writer
       */
      public void dump(PrintWriter writer) {
          NodePrinter.dump(writer, node);
      }
  
      private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
          in.defaultReadObject();
          try {
              node = builder.build(expr).getRoot();
          } catch (ELException e) {
              throw new IOException(e.getMessage());
          }
      }
  }
  