001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.util;
029
030import org.opencms.main.CmsLog;
031
032import java.beans.FeatureDescriptor;
033import java.util.Iterator;
034
035import org.apache.commons.logging.Log;
036
037import jakarta.el.ELContext;
038import jakarta.el.ELResolver;
039import jakarta.el.ImportHandler;
040import jakarta.servlet.jsp.JspContext;
041
042/**
043 * This EL resolver is a workaround for performance problems in Jetty when using EE 10.
044 * <p>
045 * When using EE 10, a new ImportELResolver is present which tries to interpret EL variables as classes. This fires for all otherwise undefined variables,
046 * of which there are many in the Mercury template at runtime, causing a lot of class loading. This leads to a huge slowdown in Jetty, but not in Tomcat.
047 * Jetty and Tomcat (currently) use the same JSP engine, which marks standalone top-level identifiers, but only Tomcat's JSP API implementation makes
048 * use of that information to optimize the lookup.
049 *
050 * <p>This resolver tries to optimize the case where it finds such a marker, effectively disabling the class lookup for standalone identifiers,
051 *  while preserving it for more complex expressions like ${Foo.bar}.
052 */
053public class CmsJspElResolver extends ELResolver {
054
055    /** The log object for this class. */
056    private static final Log LOG = CmsLog.getLog(CmsJspElResolver.class);
057
058    /**
059     * The EL context key under which the EL parser flags a stand-alone identifier,
060     * or <code>null</code> if the EL implementation in use does not provide the flag.
061     */
062    private static final Class<?> AST_IDENTIFIER_KEY;
063
064    /** The class for the NotFoundELResolver - we can't just use a class constant because it's not available in EE9 and older. */
065    private static final Class<?> NOT_FOUND_EL_RESOLVER;
066
067    static {
068        Class<?> key = null;
069        try {
070            key = Class.forName("org.apache.el.parser.AstIdentifier");
071        } catch (ClassNotFoundException e) {
072            LOG.warn(
073                "EL implementation does not flag stand-alone identifiers, JSP EL performance short-cut is disabled.",
074                e);
075        }
076        AST_IDENTIFIER_KEY = key;
077        Class<?> resolverClass = null;
078        try {
079            resolverClass = Class.forName("jakarta.servlet.jsp.el.NotFoundELResolver");
080        } catch (ClassNotFoundException e) {
081            LOG.info("Did not find NotFoundELResolver - probably Jakarta EE 9 or older", e);
082        }
083        NOT_FOUND_EL_RESOLVER = resolverClass;
084    }
085
086    /**
087     * @see jakarta.el.ELResolver#getCommonPropertyType(jakarta.el.ELContext, java.lang.Object)
088     */
089    @Override
090    public Class<?> getCommonPropertyType(ELContext context, Object base) {
091
092        return base == null ? String.class : null;
093    }
094
095    /**
096     * Dummy implementation that does nothing.
097     *
098     * (this is only needed for Java EE 8 compatibility - in Java EE 8, it actually overrides a method from the parent class.)
099     *
100     * @param context the EL context
101     * @param base the base object
102     * @return null
103     */
104    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
105
106        return null;
107    }
108
109    /**
110     * @see jakarta.el.ELResolver#getType(jakarta.el.ELContext, java.lang.Object, java.lang.Object)
111     */
112    @Override
113    public Class<?> getType(ELContext context, Object base, Object property) {
114
115        return null;
116    }
117
118    /**
119     * Resolves a stand-alone identifier from the scoped JSP attributes and ends the resolution,
120     * so that the identifier is not looked up as a class name.<p>
121     *
122     * All other cases are left to the standard resolver chain.<p>
123     *
124     * @see jakarta.el.ELResolver#getValue(jakarta.el.ELContext, java.lang.Object, java.lang.Object)
125     */
126    @Override
127    public Object getValue(ELContext context, Object base, Object property) {
128
129        if ((AST_IDENTIFIER_KEY == null) || (NOT_FOUND_EL_RESOLVER == null) || (base != null) || (property == null)) {
130            return null;
131        }
132        if (!Boolean.TRUE.equals(context.getContext(AST_IDENTIFIER_KEY))) {
133            // not a stand-alone identifier, the identifier may well be an imported class name
134            return null;
135        }
136
137        if (Boolean.TRUE.equals(context.getContext(NOT_FOUND_EL_RESOLVER))) {
138            // the page uses errorOnELNotFound, the chain must reach the NotFoundELResolver
139            return null;
140        }
141        JspContext jspContext = (JspContext)context.getContext(JspContext.class);
142        if (jspContext == null) {
143            return null;
144        }
145        String name = property.toString();
146        Object result = jspContext.findAttribute(name);
147        if (result == null) {
148            // a stand-alone identifier may still be an imported static member, this is a plain map lookup
149            result = getImportedStaticValue(context, name);
150        }
151        context.setPropertyResolved(true);
152        return result;
153    }
154
155    /**
156     * @see jakarta.el.ELResolver#isReadOnly(jakarta.el.ELContext, java.lang.Object, java.lang.Object)
157     */
158    @Override
159    public boolean isReadOnly(ELContext context, Object base, Object property) {
160
161        return false;
162    }
163
164    /**
165     * @see jakarta.el.ELResolver#setValue(jakarta.el.ELContext, java.lang.Object, java.lang.Object, java.lang.Object)
166     */
167    @Override
168    public void setValue(ELContext context, Object base, Object property, Object value) {
169
170        // not resolved here, the standard scoped attribute resolver handles writing
171    }
172
173    /**
174     * Returns the value of an imported static member with the given name, or <code>null</code> if there is none.<p>
175     *
176     * @param context the EL context
177     * @param name the name of the static member
178     *
179     * @return the value of the imported static member, or <code>null</code>
180     */
181    private Object getImportedStaticValue(ELContext context, String name) {
182
183        ImportHandler importHandler = context.getImportHandler();
184        if (importHandler == null) {
185            return null;
186        }
187        Class<?> clazz = importHandler.resolveStatic(name);
188        if (clazz == null) {
189            return null;
190        }
191        try {
192            return clazz.getField(name).get(null);
193        } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
194            // this is checked when the static import is declared, so it should not happen here
195            LOG.debug("Unable to read imported static member \"" + name + "\".", e);
196            return null;
197        }
198    }
199
200}