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.ExpressionFactory;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.lang.reflect.Constructor;
  import java.util.EnumSet;
  import java.util.Properties;
  import org.activiti.core.el.juel.misc.TypeConverter;
  import org.activiti.core.el.juel.tree.TreeBuilder;
  import org.activiti.core.el.juel.tree.TreeStore;
  import org.activiti.core.el.juel.tree.impl.Builder;
  import org.activiti.core.el.juel.tree.impl.Cache;
  
  /**
   * Expression factory implementation.
   *
   * This class is also used as an EL "service provider". The <em>juel-spi</em> jar file specifies this
   * class as el expression factory implementation in
   * <code>META-INF/services/jakarta.el.ExpressionFactory</code>. Calling
   * {@link ExpressionFactory#newInstance()} will then return an instance of this class, configured as
   * described below.
   *
   * If no properties are specified at construction time, properties are read from
   * <ol>
   * <li>
   * If the file <code>JAVA_HOME/lib/el.properties</code> exists and if it contains property
   * <code>jakarta.el.ExpressionFactory</code> whose value is the name of this class, these properties
   * are taken as default properties.</li>
   * <li>Otherwise, if system property <code>jakarta.el.ExpressionFactory</code> is set to the name of
   * this class, the system properties {@link System#getProperties()} are taken as default properties.
   * </li>
   * <li>
   * <code>el.properties</code> on your classpath. These properties override the properties from
   * <code>JAVA_HOME/lib/el.properties</code> or {@link System#getProperties()}.</li>
   * </ol>
   * There are also constructors to explicitly pass in an instance of {@link Properties}.
   *
   * Having this, the following properties are read:
   * <ul>
   * <li>
   * <code>jakarta.el.cacheSize</code> - cache size (int, default is 1000)</li>
   * <li>
   * <code>activiti.juel.methodInvocations</code> - allow method invocations as in
   * <code>${foo.bar(baz)}</code> (boolean, default is <code>false</code>).</li>
   * <li>
   * <code>activiti.juel.nullProperties</code> - resolve <code>null</code> properties as in
   * <code>${foo[null]}</code> (boolean, default is <code>false</code>).</li>
   * <li>
   * <code>activiti.juel.varArgs</code> - support function/method calls using varargs (boolean, default is
   * <code>false</code>).</li>
   * </ul>
   *
   * @author Christoph Beck
   */
  public class ExpressionFactoryImpl extends ExpressionFactory {
  
      /**
       * A profile provides a default set of language features that will define the builder's
       * behavior. A profile can be adjusted using the <code>activiti.juel.methodInvocations</code>,
       * <code>activiti.juel.varArgs</code> and <code>activiti.juel.nullProperties</code> properties.
       *
       * @since 2.2
       */
      public static enum Profile {
          /**
           * JEE5: none
           */
          JEE5(EnumSet.noneOf(Builder.Feature.class)),
          /**
           * JEE6: <code>activiti.juel.methodInvocations</code>, <code>activiti.juel.varArgs</code>. This is the
           * default profile.
           */
          JEE6(EnumSet.of(Builder.Feature.METHOD_INVOCATIONS, Builder.Feature.VARARGS));
  
          private final EnumSet<Builder.Feature> features;
  
          private Profile(EnumSet<Builder.Feature> features) {
              this.features = features;
          }
  
          Builder.Feature[] features() {
              return features.toArray(new Builder.Feature[features.size()]);
          }
  
          boolean contains(Builder.Feature feature) {
              return features.contains(feature);
          }
      }
  
      /**
       * <code>activiti.juel.methodInvocations</code>
       */
      public static final String PROP_METHOD_INVOCATIONS = "activiti.juel.methodInvocations";
  
      /**
       * <code>activiti.juel.varArgs</code>
       */
      public static final String PROP_VAR_ARGS = "activiti.juel.varArgs";
  
      /**
       * <code>activiti.juel.nullProperties</code>
       */
      public static final String PROP_NULL_PROPERTIES = "activiti.juel.nullProperties";
  
      /**
       * <code>jakarta.el.ignoreReturnType</code>
       */
      public static final String PROP_IGNORE_RETURN_TYPE = "jakarta.el.ignoreReturnType";
  
      /**
       * <code>jakarta.el.cacheSize</code>
       */
      public static final String PROP_CACHE_SIZE = "jakarta.el.cacheSize";
  
      private final TreeStore store;
      private final TypeConverter converter;
  
      /**
       * Create a new expression factory using the default builder and cache implementations. The
       * builder and cache are configured from <code>el.properties</code> (see above). The maximum
       * cache size will be 1000 unless overridden in <code>el.properties</code>. The builder profile
       * is {@link Profile#JEE6} (features may be overridden in <code>el.properties</code>).
       */
      public ExpressionFactoryImpl() {
          this(Profile.JEE6, System.getProperties());
      }
  
      /**
       * Create a new expression factory using the default builder and cache implementations. The
       * builder and cache are configured from the specified profile and <code>el.properties</code>
       * (see above). The maximum cache size will be 1000 unless overridden in
       * <code>el.properties</code>.
       *
       * @param profile
       *            builder profile (features may be overridden in <code>el.properties</code>)
       *
       * @since 2.2
       */
      public ExpressionFactoryImpl(Profile profile) {
          Properties properties = loadProperties("el.properties");
          this.store = createTreeStore(1000, profile, properties);
          this.converter = createTypeConverter(properties);
      }
  
      /**
       * Create a new expression factory using the default builder and cache implementations. The
       * builder and cache are configured using the specified properties. The maximum cache size will
       * be 1000 unless overridden by property <code>jakarta.el.cacheSize</code>. The builder profile is
       * {@link Profile#JEE6} (features may be overridden in <code>properties</code>).
       *
       * @param properties
       *            used to initialize this factory (may be <code>null</code>)
       */
      public ExpressionFactoryImpl(Properties properties) {
          this(Profile.JEE6, properties);
      }
  
      /**
       * Create a new expression factory using the default builder and cache implementations. The
       * builder and cache are configured using the specified profile and properties. The maximum
       * cache size will be 1000 unless overridden by property <code>jakarta.el.cacheSize</code>.
       *
       * @param profile
       *            builder profile (individual features may be overridden in properties)
       * @param properties
       *            used to initialize this factory (may be <code>null</code>)
       *
       * @since 2.2
       */
      public ExpressionFactoryImpl(Profile profile, Properties properties) {
          this.store = createTreeStore(1000, profile, properties);
          this.converter = createTypeConverter(properties);
      }
  
      /**
       * Create a new expression factory using the default builder and cache implementations. The
       * builder and cache are configured using the specified properties. The maximum cache size will
       * be 1000 unless overridden by property <code>jakarta.el.cacheSize</code>. The builder profile is
       * {@link Profile#JEE6} (individual features may be overridden in <code>properties</code>).
       *
       * @param properties
       *            used to initialize this factory (may be <code>null</code>)
       * @param converter
       *            custom type converter
       */
      public ExpressionFactoryImpl(Properties properties, TypeConverter converter) {
          this(Profile.JEE6, properties, converter);
      }
  
      /**
       * Create a new expression factory using the default builder and cache implementations. The
       * builder and cache are configured using the specified profile and properties. The maximum
       * cache size will be 1000 unless overridden by property <code>jakarta.el.cacheSize</code>.
       *
       * @param profile
       *            builder profile (individual features may be overridden in properties)
       * @param properties
       *            used to initialize this factory (may be <code>null</code>)
       * @param converter
       *            custom type converter
       *
       * @since 2.2
       */
      public ExpressionFactoryImpl(Profile profile, Properties properties, TypeConverter converter) {
          this.store = createTreeStore(1000, profile, properties);
          this.converter = converter;
      }
  
      /**
       * Create a new expression factory.
       *
       * @param store
       *            the tree store used to parse and cache parse trees.
       */
      public ExpressionFactoryImpl(TreeStore store) {
          this(store, TypeConverter.DEFAULT);
      }
  
      /**
       * Create a new expression factory.
       *
       * @param store
       *            the tree store used to parse and cache parse trees.
       * @param converter
       *            custom type converter
       */
      public ExpressionFactoryImpl(TreeStore store, TypeConverter converter) {
          this.store = store;
          this.converter = converter;
      }
  
      private Properties loadDefaultProperties() {
          String home = System.getProperty("java.home");
          String path = home + File.separator + "lib" + File.separator + "el.properties";
          File file = new File(path);
          try {
              if (file.exists()) {
                  Properties properties = new Properties();
                  InputStream input = null;
                  try {
                      properties.load(input = new FileInputStream(file));
                  } catch (IOException e) {
                      throw new ELException("Cannot read default EL properties", e);
                  } finally {
                      try {
                          input.close();
                      } catch (IOException e) {
                          // ignore...
                      }
                  }
                  if (getClass().getName().equals(properties.getProperty("jakarta.el.ExpressionFactory"))) {
                      return properties;
                  }
              }
          } catch (SecurityException e) {
              // ignore...
          }
          if (getClass().getName().equals(System.getProperty("jakarta.el.ExpressionFactory"))) {
              return System.getProperties();
          }
          return null;
      }
  
      private Properties loadProperties(String path) {
          Properties properties = new Properties(loadDefaultProperties());
  
          // try to find and load properties
          InputStream input = null;
          try {
              input = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
          } catch (SecurityException e) {
              input = ClassLoader.getSystemResourceAsStream(path);
          }
          if (input != null) {
              try {
                  properties.load(input);
              } catch (IOException e) {
                  throw new ELException("Cannot read EL properties", e);
              } finally {
                  try {
                      input.close();
                  } catch (IOException e) {
                      // ignore...
                  }
              }
          }
  
          return properties;
      }
  
      private boolean getFeatureProperty(
          Profile profile,
          Properties properties,
          Builder.Feature feature,
          String property
      ) {
          return Boolean.valueOf(properties.getProperty(property, String.valueOf(profile.contains(feature))));
      }
  
      /**
       * Create the factory's tree store. This implementation creates a new tree store using the
       * default builder and cache implementations. The builder and cache are configured using the
       * specified properties. The maximum cache size will be as specified unless overridden by
       * property <code>jakarta.el.cacheSize</code>.
       */
      protected TreeStore createTreeStore(int defaultCacheSize, Profile profile, Properties properties) {
          // create builder
          TreeBuilder builder = null;
          if (properties == null) {
              builder = createTreeBuilder(null, profile.features());
          } else {
              EnumSet<Builder.Feature> features = EnumSet.noneOf(Builder.Feature.class);
              if (getFeatureProperty(profile, properties, Builder.Feature.METHOD_INVOCATIONS, PROP_METHOD_INVOCATIONS)) {
                  features.add(Builder.Feature.METHOD_INVOCATIONS);
              }
              if (getFeatureProperty(profile, properties, Builder.Feature.VARARGS, PROP_VAR_ARGS)) {
                  features.add(Builder.Feature.VARARGS);
              }
              if (getFeatureProperty(profile, properties, Builder.Feature.NULL_PROPERTIES, PROP_NULL_PROPERTIES)) {
                  features.add(Builder.Feature.NULL_PROPERTIES);
              }
              if (getFeatureProperty(profile, properties, Builder.Feature.IGNORE_RETURN_TYPE, PROP_IGNORE_RETURN_TYPE)) {
                  features.add(Builder.Feature.IGNORE_RETURN_TYPE);
              }
              builder = createTreeBuilder(properties, features.toArray(new Builder.Feature[0]));
          }
  
          // create cache
          int cacheSize = defaultCacheSize;
          if (properties != null && properties.containsKey(PROP_CACHE_SIZE)) {
              try {
                  cacheSize = Integer.parseInt(properties.getProperty(PROP_CACHE_SIZE));
              } catch (NumberFormatException e) {
                  throw new ELException("Cannot parse EL property " + PROP_CACHE_SIZE, e);
              }
          }
          Cache cache = cacheSize > 0 ? new Cache(cacheSize) : null;
  
          return new TreeStore(builder, cache);
      }
  
      /**
       * Create the factory's type converter. This implementation takes the
       * <code>misc.org.activiti.core.el.juel.TypeConverter</code> property as the name of a class implementing
       * the <code>misc.org.activiti.core.el.juel.TypeConverter</code> interface. If the property is not set, the
       * default converter (<code>TypeConverter.DEFAULT</code>) is used.
       */
      protected TypeConverter createTypeConverter(Properties properties) {
          Class<?> clazz = load(TypeConverter.class, properties);
          if (clazz == null) {
              return TypeConverter.DEFAULT;
          }
          try {
              return TypeConverter.class.cast(clazz.newInstance());
          } catch (Exception e) {
              throw new ELException("TypeConverter " + clazz + " could not be instantiated", e);
          }
      }
  
      /**
       * Create the factory's builder. This implementation takes the
       * <code>tree.org.activiti.core.el.juel.TreeBuilder</code> property as a name of a class implementing the
       * <code>tree.org.activiti.core.el.juel.TreeBuilder</code> interface. If the property is not set, a plain
       * <code>impl.tree.org.activiti.core.el.juel.Builder</code> is used. If the configured class is a subclass
       * of <code>impl.tree.org.activiti.core.el.juel.Builder</code> and which provides a constructor taking an
       * array of <code>Builder.Feature</code>, this constructor will be invoked. Otherwise, the
       * default constructor will be used.
       */
      protected TreeBuilder createTreeBuilder(Properties properties, Builder.Feature... features) {
          Class<?> clazz = load(TreeBuilder.class, properties);
          if (clazz == null) {
              return new Builder(features);
          }
          try {
              if (Builder.class.isAssignableFrom(clazz)) {
                  Constructor<?> constructor = clazz.getConstructor(Builder.Feature[].class);
                  if (constructor == null) {
                      if (features == null || features.length == 0) {
                          return TreeBuilder.class.cast(clazz.newInstance());
                      } else {
                          throw new ELException("Builder " + clazz + " is missing constructor (can't pass features)");
                      }
                  } else {
                      return TreeBuilder.class.cast(constructor.newInstance((Object) features));
                  }
              } else {
                  return TreeBuilder.class.cast(clazz.newInstance());
              }
          } catch (Exception e) {
              throw new ELException("TreeBuilder " + clazz + " could not be instantiated", e);
          }
      }
  
      private Class<?> load(Class<?> clazz, Properties properties) {
          if (properties != null) {
              String className = properties.getProperty(clazz.getName());
              if (className != null) {
                  ClassLoader loader;
                  try {
                      loader = Thread.currentThread().getContextClassLoader();
                  } catch (Exception e) {
                      throw new ELException("Could not get context class loader", e);
                  }
                  try {
                      return loader == null ? Class.forName(className) : loader.loadClass(className);
                  } catch (ClassNotFoundException e) {
                      throw new ELException("Class " + className + " not found", e);
                  } catch (Exception e) {
                      throw new ELException("Class " + className + " could not be instantiated", e);
                  }
              }
          }
          return null;
      }
  
      @Override
      public final <T> T coerceToType(Object obj, Class<T> targetType) {
          return converter.convert(obj, targetType);
      }
  
      @Override
      public final ObjectValueExpression createValueExpression(Object instance, Class<?> expectedType) {
          return new ObjectValueExpression(converter, instance, expectedType);
      }
  
      @Override
      public final TreeValueExpression createValueExpression(
          ELContext context,
          String expression,
          Class<?> expectedType
      ) {
          return new TreeValueExpression(
              store,
              context.getFunctionMapper(),
              context.getVariableMapper(),
              converter,
              expression,
              expectedType
          );
      }
  
      @Override
      public final TreeMethodExpression createMethodExpression(
          ELContext context,
          String expression,
          Class<?> expectedReturnType,
          Class<?>[] expectedParamTypes
      ) {
          return new TreeMethodExpression(
              store,
              context.getFunctionMapper(),
              context.getVariableMapper(),
              converter,
              expression,
              expectedReturnType,
              expectedParamTypes
          );
      }
  }
  