diff --git a/api/src/main/java/jakarta/el/ELResolver.java b/api/src/main/java/jakarta/el/ELResolver.java index a9729be1..9f0ea7a6 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 Object invoke(ELContext context, Object base, Object method, Class[] p public T convertToType(ELContext context, Object obj, Class 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.ImportELResolver} - to optimise the resolution of the identifier and avoid + * unnecessary and expensive class loader lookups. + *

+ * 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 a8ee2160..77e3f592 100644 --- a/spec/src/main/asciidoc/ELSpec.adoc +++ b/spec/src/main/asciidoc/ELSpec.adoc @@ -560,6 +560,20 @@ One implication of the explicit search order 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.ImportELResolver`. + +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 @@ This appendix is non-normative. === 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.ImportELResolver`. + * 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 5adce647..6574bcc7 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -62,6 +62,7 @@ jakarta.el jakarta.el-api + 6.1.0-SNAPSHOT org.junit.jupiter 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 f326e995..87fa7536 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 @@ private ELResolver getMyResolver(ResolverType enumResolver) { 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 00000000..0c16d5b3 --- /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 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 f8beb2cc..9f6959ef 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 b22de123..3d3c5732 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 @@ public void parseOnceEvalManyTest() throws Exception { 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!"); + } }