Fix #211 - add context marker for single, stand-alone identifiers
The presence of this marker will allow resolvers, particularly
jakarta.servlet.jsp.el.ScopedAttributeELResolver to optimise lookups.
diff --git a/api/src/main/java/jakarta/el/ELResolver.java b/api/src/main/java/jakarta/el/ELResolver.java
index a9729be..453b768 100644
--- a/api/src/main/java/jakarta/el/ELResolver.java
+++ b/api/src/main/java/jakarta/el/ELResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1997, 2024 Oracle and/or its affiliates and others.
+ * Copyright (c) 1997, 2025 Oracle and/or its affiliates and others.
* All rights reserved.
* Copyright 2004 The Apache Software Foundation
*
@@ -265,4 +265,23 @@
public <T> T convertToType(ELContext context, Object obj, Class<T> targetType) {
return null;
}
+
+ /**
+ * This class is used as a key for {@link ELContext#getContext(Class)}. The key references a context object that if
+ * present and set to {@code Boolean#TRUE}, indicates that the identifier being resolved is a single, stand-alone
+ * identifier. This allows {@link ELResolver} instances - and in particular
+ * {@code jakarta.servlet.jsp.el.ScopedAttributeELResolver} - to optimise the resolution of the identifier and avoid
+ * unnecessary and expensive class loader lookups.
+ * <p>
+ * The EL implementation is required to set this key with the value {@code Boolean#TRUE} when resolving a single,
+ * stand-alone identifier.
+ *
+ * @since Jakarta Expression Language 6.1
+ */
+ public class StandaloneIdentifierMarker {
+
+ private StandaloneIdentifierMarker() {
+ // Non-public default constructor as there is no need to create instances of this class.
+ }
+ }
}
diff --git a/spec/src/main/asciidoc/ELSpec.adoc b/spec/src/main/asciidoc/ELSpec.adoc
index a8ee216..642ce81 100644
--- a/spec/src/main/asciidoc/ELSpec.adoc
+++ b/spec/src/main/asciidoc/ELSpec.adoc
@@ -560,6 +560,20 @@
of the identifiers is that an identifier hides other identifiers (of the
same name) that come after it in the list.
+===== Resolving Identifiers with ``ELResolver``s
+
+Resolution of identifiers with ``ELResolver``s can be optimised when a single,
+stand-alone identifier (e.g. `identifier-a`) needs to be resolved compared to
+the resolution of multiple identifiers (e.g. `identifier-a.identifier-b`). This
+is particularly important for applications using Jakarta Pages as optimisation
+enables measurable performance improvements for instances of
+`jakarta.servlet.jsp.el.ScopedAttributeELResolver`.
+
+To support such optimisations, when resolving a single, stand-alone identifier,
+the implementation is required to put an instance of `Boolean.TRUE` into the
+`ELContext` under the key `jakarta.el.ELResolver.StandaloneIdentifierMarker` for
+the duration of the attempt to resolve the identifier.
+
==== Evaluating functions
The expression with the syntax
@@ -3000,6 +3014,12 @@
=== Changes between 6.1 and 6.0
+* https://github.com/jakartaee/expression-language/issues/211[#211]
+ Provider a marker in the `ELContext` when resolving single, stand-alone
+ identifiers that allows an `ELResolver` to optimise the resolution of the
+ identifier. This change supports a performance improvement in
+ `jakarta.servlet.jsp.el.ScopedAttributeELResolver`.
+
* https://github.com/jakartaee/expression-language/issues/313[#313]
Add support for inner classes when using the `ImportHandler` and clarify that
the import handler expects canonical class names where full class names are
diff --git a/tck/pom.xml b/tck/pom.xml
index e31bd58..ee8c7d4 100644
--- a/tck/pom.xml
+++ b/tck/pom.xml
@@ -62,6 +62,7 @@
<dependency>
<groupId>jakarta.el</groupId>
<artifactId>jakarta.el-api</artifactId>
+ <version>6.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
diff --git a/tck/src/main/java/com/sun/ts/tests/el/common/elcontext/SimpleELContext.java b/tck/src/main/java/com/sun/ts/tests/el/common/elcontext/SimpleELContext.java
index f326e99..87fa753 100644
--- a/tck/src/main/java/com/sun/ts/tests/el/common/elcontext/SimpleELContext.java
+++ b/tck/src/main/java/com/sun/ts/tests/el/common/elcontext/SimpleELContext.java
@@ -22,10 +22,12 @@
import com.sun.ts.tests.el.common.elresolver.EmployeeELResolver;
+import com.sun.ts.tests.el.common.elresolver.SingleIdentifierELResolver;
import com.sun.ts.tests.el.common.elresolver.VariableELResolver;
import com.sun.ts.tests.el.common.elresolver.VectELResolver;
import com.sun.ts.tests.el.common.util.ResolverType;
+import jakarta.el.BeanELResolver;
import jakarta.el.CompositeELResolver;
import jakarta.el.ELContext;
import jakarta.el.ELResolver;
@@ -138,6 +140,13 @@
logger.log(Logger.Level.TRACE, "Setting ELResolver == VectELResolver");
break;
+ case SINGLE_IDENTIFER_ELRESOLVER:
+ myResolver = new CompositeELResolver();
+ ((CompositeELResolver) myResolver).add(new SingleIdentifierELResolver());
+ ((CompositeELResolver) myResolver).add(new BeanELResolver());
+ logger.log(Logger.Level.TRACE, "Setting ELResolver == SingleIdentifierELResolver");
+ break;
+
default:
logger.log(Logger.Level.TRACE,
"Unknown ELResolver! " + enumResolver + " trying to use default"
diff --git a/tck/src/main/java/com/sun/ts/tests/el/common/elresolver/SingleIdentifierELResolver.java b/tck/src/main/java/com/sun/ts/tests/el/common/elresolver/SingleIdentifierELResolver.java
new file mode 100644
index 0000000..0c16d5b
--- /dev/null
+++ b/tck/src/main/java/com/sun/ts/tests/el/common/elresolver/SingleIdentifierELResolver.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package com.sun.ts.tests.el.common.elresolver;
+
+import java.util.Set;
+
+import jakarta.el.ELContext;
+import jakarta.el.ELResolver;
+
+/*
+ * This ELResolver resolves the identifiers "single" and "notSingle".
+ *
+ * For "single", the StandaloneIdentifierMarker MUST be present and MUST be true to resolve the identifier to PASS.
+ * Otherwise it resolves to FAIL.
+ *
+ * For "notSingle", the StandaloneIdentifierMarker MAY be present and if present MUST be false to resolve the identifier
+ * to PASS. Otherwise it resolves to FAIL.
+ */
+public class SingleIdentifierELResolver extends ELResolver {
+
+ public static final String SINGLE = "single";
+ public static final String NOT_SINGLE = "notSingle";
+
+ private static final Set<String> IDENTIFIERS = Set.of(SINGLE, NOT_SINGLE);
+
+ public static final String FAIL = "NOT_OK";
+ public static final String PASS = "OK";
+
+ @Override
+ public Object getValue(ELContext context, Object base, Object property) {
+ if (!willResolve(context, base, property)) {
+ return null;
+ }
+
+ Object marker = context.getContext(ELResolver.StandaloneIdentifierMarker.class);
+
+ if (marker == null) {
+ if (NOT_SINGLE.equals(property)) {
+ return PASS;
+ }
+ return FAIL;
+ }
+
+ if (!(marker instanceof Boolean b)) {
+ return FAIL;
+ }
+
+
+ if (SINGLE.equals(property)) {
+ if (b.booleanValue()) {
+ return PASS;
+ }
+ return FAIL;
+ }
+
+ if (NOT_SINGLE.equals(property)) {
+ if (b.booleanValue()) {
+ return FAIL;
+ }
+ return PASS;
+ }
+
+ // Shouldn't reach here but fail if we do
+ return FAIL;
+ }
+
+ @Override
+ public Class<?> getType(ELContext context, Object base, Object property) {
+ if (!willResolve(context, base, property)) {
+ return null;
+ }
+ return String.class;
+ }
+
+ @Override
+ public void setValue(ELContext context, Object base, Object property, Object value) {
+ // NO-OP
+ }
+
+ @Override
+ public boolean isReadOnly(ELContext context, Object base, Object property) {
+ if (!willResolve(context, base, property)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public Class<?> getCommonPropertyType(ELContext context, Object base) {
+ return String.class;
+ }
+
+ private boolean willResolve(ELContext context, Object base, Object property) {
+ if (base == null && IDENTIFIERS.contains(property)) {
+ context.setPropertyResolved(true);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/tck/src/main/java/com/sun/ts/tests/el/common/util/ResolverType.java b/tck/src/main/java/com/sun/ts/tests/el/common/util/ResolverType.java
index f8beb2c..9f6959e 100644
--- a/tck/src/main/java/com/sun/ts/tests/el/common/util/ResolverType.java
+++ b/tck/src/main/java/com/sun/ts/tests/el/common/util/ResolverType.java
@@ -1,5 +1,6 @@
/*
- * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2009, 2025 Oracle and/or its affiliates and others.
+ * All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -13,13 +14,8 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
-
-/*
- * $Id$
- */
-
package com.sun.ts.tests.el.common.util;
public enum ResolverType {
- EMPLOYEE_ELRESOLVER, VARIABLE_ELRESOLVER, VECT_ELRESOLVER;
+ EMPLOYEE_ELRESOLVER, VARIABLE_ELRESOLVER, VECT_ELRESOLVER, SINGLE_IDENTIFER_ELRESOLVER;
}
diff --git a/tck/src/main/java/com/sun/ts/tests/el/spec/language/ELClientIT.java b/tck/src/main/java/com/sun/ts/tests/el/spec/language/ELClientIT.java
index b22de12..3d3c573 100644
--- a/tck/src/main/java/com/sun/ts/tests/el/spec/language/ELClientIT.java
+++ b/tck/src/main/java/com/sun/ts/tests/el/spec/language/ELClientIT.java
@@ -23,8 +23,9 @@
import java.util.Hashtable;
-import com.sun.ts.tests.el.common.util.ELTestUtil;
+import com.sun.ts.tests.el.common.elresolver.SingleIdentifierELResolver;
import com.sun.ts.tests.el.common.spec.Book;
+import com.sun.ts.tests.el.common.util.ELTestUtil;
import com.sun.ts.tests.el.common.util.ExprEval;
import com.sun.ts.tests.el.common.util.ResolverType;
@@ -606,4 +607,37 @@
throw new Exception("TEST FAILED!");
}
+ @Test
+ public void optimiseStandaloneIdentifier() throws Exception {
+ boolean pass = false;
+
+ String expr = "${" + SingleIdentifierELResolver.SINGLE + "}";
+ String expected = SingleIdentifierELResolver.PASS;
+
+ try {
+ pass = expected.equals(
+ ExprEval.evaluateValueExpression(expr, null, String.class, ResolverType.SINGLE_IDENTIFER_ELRESOLVER));
+ } catch (Exception e) {
+ throw new Exception(e);
+ }
+ if (!pass)
+ throw new Exception("TEST FAILED!");
+ }
+
+ @Test
+ public void optimiseStandaloneIdentifierNegative() throws Exception {
+ boolean pass = false;
+
+ String expr = "${" + SingleIdentifierELResolver.NOT_SINGLE + ".length()}";
+ Integer expected = Integer.valueOf(SingleIdentifierELResolver.PASS.length());
+
+ try {
+ pass = expected.equals(
+ ExprEval.evaluateValueExpression(expr, null, Integer.class, ResolverType.SINGLE_IDENTIFER_ELRESOLVER));
+ } catch (Exception e) {
+ throw new Exception(e);
+ }
+ if (!pass)
+ throw new Exception("TEST FAILED!");
+ }
}