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!");
+  }
 }