/* * ============================================================================= * * Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org) * * 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.thymeleaf.context; import java.lang.reflect.Array; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.thymeleaf.IEngineConfiguration; import org.thymeleaf.engine.TemplateData; import org.thymeleaf.inline.IInliner; import org.thymeleaf.inline.NoOpInliner; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.util.Validate; /** * <p> * Basic <b>web</b> implementation of the {@link IEngineContext} interface, based on the Servlet API. * </p> * <p> * This is the context implementation that will be used by default for web processing. Note that <b>this is an * internal implementation, and there is no reason for users' code to directly reference or use it instead * of its implemented interfaces</b>. * </p> * <p> * This class is NOT thread-safe. Thread-safety is not a requirement for context implementations. * </p> * * @author Daniel Fernández * * @since 3.0.0 * */ public class WebEngineContext extends AbstractEngineContext implements IEngineContext, IWebContext { /* * --------------------------------------------------------------------------- * THIS MAP FORWARDS ALL OPERATIONS TO THE UNDERLYING REQUEST, EXCEPT * FOR THE param (request parameters), session (session attributes) AND * application (servlet context attributes) VARIABLES. * * NOTE that, even if attributes are leveled so that above level 0 they are * considered local and thus disappear after lowering the level, attributes * directly set on the request object are considered global and therefore * valid even when the level decreased (though they can be overridden). This * is so for better simulating the effect of directly working against the * request object, and for better integration with JSP or any other template * engines or view-layer technologies that expect the HttpServletRequest to * be the 'only source of truth' for context variables. * --------------------------------------------------------------------------- */ private static final String PARAM_VARIABLE_NAME = "param"; private static final String SESSION_VARIABLE_NAME = "session"; private static final String APPLICATION_VARIABLE_NAME = "application"; private final HttpServletRequest request; private final HttpServletResponse response; private final HttpSession session; private final ServletContext servletContext; private final RequestAttributesVariablesMap requestAttributesVariablesMap; private final Map<String,Object> requestParametersVariablesMap; private final Map<String,Object> sessionAttributesVariablesMap; private final Map<String,Object> applicationAttributesVariablesMap; /** * <p> * Creates a new instance of this {@link IEngineContext} implementation binding engine execution to * the Servlet API. * </p> * <p> * Note that implementations of {@link IEngineContext} are not meant to be used in order to call * the template engine (use implementations of {@link IContext} such as {@link Context} or {@link WebContext} * instead). This is therefore mostly an <b>internal</b> implementation, and users should have no reason * to ever call this constructor except in very specific integration/extension scenarios. * </p> * * @param configuration the configuration instance being used. * @param templateData the template data for the template to be processed. * @param templateResolutionAttributes the template resolution attributes. * @param request the servlet request object. * @param response the servlet response object. * @param servletContext the servlet context object. * @param locale the locale. * @param variables the context variables, probably coming from another {@link IContext} implementation. */ public WebEngineContext( final IEngineConfiguration configuration, final TemplateData templateData, final Map<String,Object> templateResolutionAttributes, final HttpServletRequest request, final HttpServletResponse response, final ServletContext servletContext, final Locale locale, final Map<String, Object> variables) { super(configuration, templateResolutionAttributes, locale); Validate.notNull(request, "Request cannot be null in web variables map"); Validate.notNull(response, "Response cannot be null in web variables map"); Validate.notNull(servletContext, "Servlet Context cannot be null in web variables map"); this.request = request; this.response = response; this.session = request.getSession(false); this.servletContext = servletContext; this.requestAttributesVariablesMap = new RequestAttributesVariablesMap(configuration, templateData, templateResolutionAttributes, this.request, locale, variables); this.requestParametersVariablesMap = new RequestParametersMap(this.request); this.applicationAttributesVariablesMap = new ServletContextAttributesMap(this.servletContext); this.sessionAttributesVariablesMap = new SessionAttributesMap(this.session); } public HttpServletRequest getRequest() { return this.request; } public HttpServletResponse getResponse() { return this.response; } public HttpSession getSession() { return this.session; } public ServletContext getServletContext() { return this.servletContext; } public boolean containsVariable(final String name) { if (SESSION_VARIABLE_NAME.equals(name)) { return this.sessionAttributesVariablesMap != null; } if (PARAM_VARIABLE_NAME.equals(name)) { return true; } return APPLICATION_VARIABLE_NAME.equals(name) || this.requestAttributesVariablesMap.containsVariable(name); } public Object getVariable(final String key) { if (SESSION_VARIABLE_NAME.equals(key)) { return this.sessionAttributesVariablesMap; } if (PARAM_VARIABLE_NAME.equals(key)) { return this.requestParametersVariablesMap; } if (APPLICATION_VARIABLE_NAME.equals(key)) { return this.applicationAttributesVariablesMap; } return this.requestAttributesVariablesMap.getVariable(key); } public Set<String> getVariableNames() { // Note this set will NOT include 'param', 'session' or 'application', as they are considered special // ways to access attributes/parameters in these Servlet API structures return this.requestAttributesVariablesMap.getVariableNames(); } public void setVariable(final String name, final Object value) { if (SESSION_VARIABLE_NAME.equals(name) || PARAM_VARIABLE_NAME.equals(name) || APPLICATION_VARIABLE_NAME.equals(name)) { throw new IllegalArgumentException( "Cannot set variable called '" + name + "' into web variables map: such name is a reserved word"); } this.requestAttributesVariablesMap.setVariable(name, value); } public void setVariables(final Map<String, Object> variables) { if (variables == null || variables.isEmpty()) { return; } // First perform reserved word check on every variable name to be inserted for (final String name : variables.keySet()) { if (SESSION_VARIABLE_NAME.equals(name) || PARAM_VARIABLE_NAME.equals(name) || APPLICATION_VARIABLE_NAME.equals(name)) { throw new IllegalArgumentException( "Cannot set variable called '" + name + "' into web variables map: such name is a reserved word"); } } this.requestAttributesVariablesMap.setVariables(variables); } public void removeVariable(final String name) { if (SESSION_VARIABLE_NAME.equals(name) || PARAM_VARIABLE_NAME.equals(name) || APPLICATION_VARIABLE_NAME.equals(name)) { throw new IllegalArgumentException( "Cannot remove variable called '" + name + "' in web variables map: such name is a reserved word"); } this.requestAttributesVariablesMap.removeVariable(name); } public boolean isVariableLocal(final String name) { return this.requestAttributesVariablesMap.isVariableLocal(name); } public boolean hasSelectionTarget() { return this.requestAttributesVariablesMap.hasSelectionTarget(); } public Object getSelectionTarget() { return this.requestAttributesVariablesMap.getSelectionTarget(); } public void setSelectionTarget(final Object selectionTarget) { this.requestAttributesVariablesMap.setSelectionTarget(selectionTarget); } public IInliner getInliner() { return this.requestAttributesVariablesMap.getInliner(); } public void setInliner(final IInliner inliner) { this.requestAttributesVariablesMap.setInliner(inliner); } public TemplateData getTemplateData() { return this.requestAttributesVariablesMap.getTemplateData(); } public void setTemplateData(final TemplateData templateData) { this.requestAttributesVariablesMap.setTemplateData(templateData); } public List<TemplateData> getTemplateStack() { return this.requestAttributesVariablesMap.getTemplateStack(); } public void setElementTag(final IProcessableElementTag elementTag) { this.requestAttributesVariablesMap.setElementTag(elementTag); } public List<IProcessableElementTag> getElementStack() { return this.requestAttributesVariablesMap.getElementStack(); } public List<IProcessableElementTag> getElementStackAbove(final int contextLevel) { return this.requestAttributesVariablesMap.getElementStackAbove(contextLevel); } public int level() { return this.requestAttributesVariablesMap.level(); } public void increaseLevel() { this.requestAttributesVariablesMap.increaseLevel(); } public void decreaseLevel() { this.requestAttributesVariablesMap.decreaseLevel(); } public String getStringRepresentationByLevel() { // Request parameters, session and servlet context can be safely ignored here return this.requestAttributesVariablesMap.getStringRepresentationByLevel(); } @Override public String toString() { // Request parameters, session and servlet context can be safely ignored here return this.requestAttributesVariablesMap.toString(); } static Object resolveLazy(final Object variable) { /* * Check the possibility that this variable is a lazy one, in which case we should not return it directly * but instead make sure it is initialized and return its value. */ if (variable != null && variable instanceof ILazyContextVariable) { return ((ILazyContextVariable)variable).getValue(); } return variable; } private static final class SessionAttributesMap extends NoOpMapImpl { private final HttpSession session; SessionAttributesMap(final HttpSession session) { super(); this.session = session; } @Override public int size() { if (this.session == null) { return 0; } int size = 0; final Enumeration<String> attributeNames = this.session.getAttributeNames(); while (attributeNames.hasMoreElements()) { attributeNames.nextElement(); size++; } return size; } @Override public boolean isEmpty() { if (this.session == null) { return true; } final Enumeration<String> attributeNames = this.session.getAttributeNames(); return attributeNames.hasMoreElements(); } @Override public boolean containsKey(final Object key) { // Even if not completely correct to return 'true' for entries that might not exist, this is needed // in order to avoid Spring's MapAccessor throwing an exception when trying to access an element // that doesn't exist -- in the case of request parameters, session and servletContext attributes most // developers would expect null to be returned in such case, and that's what this 'true' will cause. return true; } @Override public boolean containsValue(final Object value) { // It wouldn't be consistent to have an 'ad hoc' implementation of #containsKey() but a 100% correct // implementation of #containsValue(), so we are leaving this as unsupported. throw new UnsupportedOperationException("Map does not support #containsValue()"); } @Override public Object get(final Object key) { if (this.session == null) { return null; } return resolveLazy(this.session.getAttribute(key != null? key.toString() : null)); } @Override public Set<String> keySet() { if (this.session == null) { return Collections.emptySet(); } final Set<String> keySet = new LinkedHashSet<String>(5); final Enumeration<String> attributeNames = this.session.getAttributeNames(); while (attributeNames.hasMoreElements()) { keySet.add(attributeNames.nextElement()); } return keySet; } @Override public Collection<Object> values() { if (this.session == null) { return Collections.emptySet(); } final List<Object> values = new ArrayList<Object>(5); final Enumeration<String> attributeNames = this.session.getAttributeNames(); while (attributeNames.hasMoreElements()) { values.add(this.session.getAttribute(attributeNames.nextElement())); } return values; } @Override public Set<Entry<String,Object>> entrySet() { if (this.session == null) { return Collections.emptySet(); } final Set<Entry<String,Object>> entrySet = new LinkedHashSet<Entry<String, Object>>(5); final Enumeration<String> attributeNames = this.session.getAttributeNames(); while (attributeNames.hasMoreElements()) { final String key = attributeNames.nextElement(); final Object value = this.session.getAttribute(key); entrySet.add(new MapEntry(key, value)); } return entrySet; } } private static final class ServletContextAttributesMap extends NoOpMapImpl { private final ServletContext servletContext; ServletContextAttributesMap(final ServletContext servletContext) { super(); this.servletContext = servletContext; } @Override public int size() { int size = 0; final Enumeration<String> attributeNames = this.servletContext.getAttributeNames(); while (attributeNames.hasMoreElements()) { attributeNames.nextElement(); size++; } return size; } @Override public boolean isEmpty() { final Enumeration<String> attributeNames = this.servletContext.getAttributeNames(); return attributeNames.hasMoreElements(); } @Override public boolean containsKey(final Object key) { // Even if not completely correct to return 'true' for entries that might not exist, this is needed // in order to avoid Spring's MapAccessor throwing an exception when trying to access an element // that doesn't exist -- in the case of request parameters, session and servletContext attributes most // developers would expect null to be returned in such case, and that's what this 'true' will cause. return true; } @Override public boolean containsValue(final Object value) { // It wouldn't be consistent to have an 'ad hoc' implementation of #containsKey() but a 100% correct // implementation of #containsValue(), so we are leaving this as unsupported. throw new UnsupportedOperationException("Map does not support #containsValue()"); } @Override public Object get(final Object key) { return resolveLazy(this.servletContext.getAttribute(key != null? key.toString() : null)); } @Override public Set<String> keySet() { final Set<String> keySet = new LinkedHashSet<String>(5); final Enumeration<String> attributeNames = this.servletContext.getAttributeNames(); while (attributeNames.hasMoreElements()) { keySet.add(attributeNames.nextElement()); } return keySet; } @Override public Collection<Object> values() { final List<Object> values = new ArrayList<Object>(5); final Enumeration<String> attributeNames = this.servletContext.getAttributeNames(); while (attributeNames.hasMoreElements()) { values.add(this.servletContext.getAttribute(attributeNames.nextElement())); } return values; } @Override public Set<Map.Entry<String,Object>> entrySet() { final Set<Map.Entry<String,Object>> entrySet = new LinkedHashSet<Map.Entry<String, Object>>(5); final Enumeration<String> attributeNames = this.servletContext.getAttributeNames(); while (attributeNames.hasMoreElements()) { final String key = attributeNames.nextElement(); final Object value = this.servletContext.getAttribute(key); entrySet.add(new MapEntry(key, value)); } return entrySet; } } private static final class RequestParametersMap extends NoOpMapImpl { private final HttpServletRequest request; RequestParametersMap(final HttpServletRequest request) { super(); this.request = request; } @Override public int size() { return this.request.getParameterMap().size(); } @Override public boolean isEmpty() { return this.request.getParameterMap().isEmpty(); } @Override public boolean containsKey(final Object key) { // Even if not completely correct to return 'true' for entries that might not exist, this is needed // in order to avoid Spring's MapAccessor throwing an exception when trying to access an element // that doesn't exist -- in the case of request parameters, session and servletContext attributes most // developers would expect null to be returned in such case, and that's what this 'true' will cause. return true; } @Override public boolean containsValue(final Object value) { // It wouldn't be consistent to have an 'ad hoc' implementation of #containsKey() but a 100% correct // implementation of #containsValue(), so we are leaving this as unsupported. throw new UnsupportedOperationException("Map does not support #containsValue()"); } @Override public Object get(final Object key) { final String[] parameterValues = this.request.getParameterValues(key != null? key.toString() : null); if (parameterValues == null) { return null; } return new RequestParameterValues(parameterValues); } @Override public Set<String> keySet() { return this.request.getParameterMap().keySet(); } @Override public Collection<Object> values() { return this.request.getParameterMap().values(); } @Override public Set<Map.Entry<String,Object>> entrySet() { return this.request.getParameterMap().entrySet(); } } private static final class RequestAttributesVariablesMap extends AbstractEngineContext implements IEngineContext { private static final int DEFAULT_ELEMENT_HIERARCHY_SIZE = 20; private static final int DEFAULT_LEVELS_SIZE = 10; private static final int DEFAULT_LEVELARRAYS_SIZE = 5; private final HttpServletRequest request; private int level = 0; private int index = 0; private int[] levels; private String[][] names; private Object[][] oldValues; private Object[][] newValues; private int[] levelSizes; private SelectionTarget[] selectionTargets; private IInliner[] inliners; private TemplateData[] templateDatas; private IProcessableElementTag[] elementTags; private SelectionTarget lastSelectionTarget = null; private IInliner lastInliner = null; private TemplateData lastTemplateData = null; private final List<TemplateData> templateStack; RequestAttributesVariablesMap( final IEngineConfiguration configuration, final TemplateData templateData, final Map<String,Object> templateResolutionAttributes, final HttpServletRequest request, final Locale locale, final Map<String, Object> variables) { super(configuration, templateResolutionAttributes, locale); this.request = request; this.levels = new int[DEFAULT_LEVELS_SIZE]; this.names = new String[DEFAULT_LEVELS_SIZE][]; this.oldValues = new Object[DEFAULT_LEVELS_SIZE][]; this.newValues = new Object[DEFAULT_LEVELS_SIZE][]; this.levelSizes = new int[DEFAULT_LEVELS_SIZE]; this.selectionTargets = new SelectionTarget[DEFAULT_LEVELS_SIZE]; this.inliners = new IInliner[DEFAULT_LEVELS_SIZE]; this.templateDatas = new TemplateData[DEFAULT_LEVELS_SIZE]; this.elementTags = new IProcessableElementTag[DEFAULT_ELEMENT_HIERARCHY_SIZE]; Arrays.fill(this.levels, Integer.MAX_VALUE); Arrays.fill(this.names, null); Arrays.fill(this.oldValues, null); Arrays.fill(this.newValues, null); Arrays.fill(this.levelSizes, 0); Arrays.fill(this.selectionTargets, null); Arrays.fill(this.inliners, null); Arrays.fill(this.templateDatas, null); Arrays.fill(this.elementTags, null); this.levels[0] = 0; this.templateDatas[0] = templateData; this.lastTemplateData = templateData; this.templateStack = new ArrayList<TemplateData>(DEFAULT_LEVELS_SIZE); this.templateStack.add(templateData); if (variables != null) { setVariables(variables); } } public boolean containsVariable(final String name) { return this.request.getAttribute(name) != null; } public Object getVariable(final String key) { return resolveLazy(this.request.getAttribute(key)); } public Set<String> getVariableNames() { // -------------------------- // Note this method relies on HttpServletRequest#getAttributeNames(), which is an extremely slow and // inefficient method in implementations like Apache Tomcat's. So the uses of this method should be // very controlled and reduced to the minimum. Specifically, any call that executes e.g. for every // expression evaluation should be disallowed. Only sporadic uses should be done. // Note also it would not be a good idea to cache the attribute names coming from the request if we // want to keep complete independence of the HttpServletRequest object, so that it can be modified // from the outside (e.g. from other libraries like Tiles) with Thymeleaf perfectly integrating with // those modifications. // -------------------------- final Set<String> variableNames = new HashSet<String>(10); final Enumeration<String> attributeNamesEnum = this.request.getAttributeNames(); while (attributeNamesEnum.hasMoreElements()) { variableNames.add(attributeNamesEnum.nextElement()); } return variableNames; } private int searchNameInIndex(final String name, final int idx) { int n = this.levelSizes[idx]; if (name == null) { while (n-- != 0) { if (this.names[idx][n] == null) { return n; } } return -1; } while (n-- != 0) { if (name.equals(this.names[idx][n])) { return n; } } return -1; } public void setVariable(final String name, final Object value) { ensureLevelInitialized(true); if (this.level > 0) { // We will only take care of new/old values if we are not on level 0 int levelIndex = searchNameInIndex(name,this.index); if (levelIndex >= 0) { // There already is a registered movement for this key - we should modify it instead of creating a new one this.newValues[this.index][levelIndex] = value; } else { if (this.names[this.index].length == this.levelSizes[this.index]) { // We need to grow the arrays for this level this.names[this.index] = Arrays.copyOf(this.names[this.index], this.names[this.index].length + DEFAULT_LEVELARRAYS_SIZE); this.newValues[this.index] = Arrays.copyOf(this.newValues[this.index], this.newValues[this.index].length + DEFAULT_LEVELARRAYS_SIZE); this.oldValues[this.index] = Arrays.copyOf(this.oldValues[this.index], this.oldValues[this.index].length + DEFAULT_LEVELARRAYS_SIZE); } levelIndex = this.levelSizes[this.index]; // We will add at the end this.names[this.index][levelIndex] = name; /* * Per construction, according to the Servlet API, an attribute set to null and a non-existing * attribute are exactly the same. So we don't really have a reason to worry about the attribute * already existing or not when it was set to null. */ this.oldValues[this.index][levelIndex] = this.request.getAttribute(name); this.newValues[this.index][levelIndex] = value; this.levelSizes[this.index]++; } } // No matter if value is null or not. Value null will be equivalent to .removeAttribute() this.request.setAttribute(name, value); } public void setVariables(final Map<String, Object> variables) { if (variables == null || variables.isEmpty()) { return; } for (final Map.Entry<String,Object> entry : variables.entrySet()) { setVariable(entry.getKey(), entry.getValue()); } } public void removeVariable(final String name) { setVariable(name, null); } public boolean isVariableLocal(final String name) { if (this.level == 0) { // We are at level 0, so we cannot have local variables at all return false; } int n = this.index + 1; while (n-- > 1) { // variables at n == 0 are not local! final int idx = searchNameInIndex(name, n); if (idx >= 0) { return this.newValues[n][idx] != null; } } return false; } public boolean hasSelectionTarget() { if (this.lastSelectionTarget != null) { return true; } int n = this.index + 1; while (n-- != 0) { if (this.selectionTargets[n] != null) { return true; } } return false; } public Object getSelectionTarget() { if (this.lastSelectionTarget != null) { return this.lastSelectionTarget.selectionTarget; } int n = this.index + 1; while (n-- != 0) { if (this.selectionTargets[n] != null) { this.lastSelectionTarget = this.selectionTargets[n]; return this.lastSelectionTarget.selectionTarget; } } return null; } public void setSelectionTarget(final Object selectionTarget) { ensureLevelInitialized(false); this.lastSelectionTarget = new SelectionTarget(selectionTarget); this.selectionTargets[this.index] = this.lastSelectionTarget; } public IInliner getInliner() { if (this.lastInliner != null) { if (this.lastInliner == NoOpInliner.INSTANCE) { return null; } return this.lastInliner; } int n = this.index + 1; while (n-- != 0) { if (this.inliners[n] != null) { this.lastInliner = this.inliners[n]; if (this.lastInliner == NoOpInliner.INSTANCE) { return null; } return this.lastInliner; } } return null; } public void setInliner(final IInliner inliner) { ensureLevelInitialized(false); // We use NoOpInliner.INSTACE in order to signal when inlining has actually been disabled this.lastInliner = (inliner == null? NoOpInliner.INSTANCE : inliner); this.inliners[this.index] = this.lastInliner; } public TemplateData getTemplateData() { if (this.lastTemplateData != null) { return this.lastTemplateData; } int n = this.index + 1; while (n-- != 0) { if (this.templateDatas[n] != null) { this.lastTemplateData = this.templateDatas[n]; return this.lastTemplateData; } } return null; } public void setTemplateData(final TemplateData templateData) { Validate.notNull(templateData, "Template Data cannot be null"); ensureLevelInitialized(false); this.lastTemplateData = templateData; this.templateDatas[this.index] = this.lastTemplateData; this.templateStack.clear(); } public List<TemplateData> getTemplateStack() { if (!this.templateStack.isEmpty()) { // If would have been empty if we had just decreased a level or added a new template return Collections.unmodifiableList(new ArrayList<TemplateData>(this.templateStack)); } for (int i = 0; i <= this.index; i++) { if (this.templateDatas[i] != null) { this.templateStack.add(this.templateDatas[i]); } } return Collections.unmodifiableList(new ArrayList<TemplateData>(this.templateStack)); } public void setElementTag(final IProcessableElementTag elementTag) { if (this.elementTags.length <= this.level) { this.elementTags = Arrays.copyOf(this.elementTags, Math.max(this.level, this.elementTags.length + DEFAULT_ELEMENT_HIERARCHY_SIZE)); } this.elementTags[this.level] = elementTag; } public List<IProcessableElementTag> getElementStack() { final List<IProcessableElementTag> elementStack = new ArrayList<IProcessableElementTag>(this.level); for (int i = 0; i <= this.level && i < this.elementTags.length; i++) { if (this.elementTags[i] != null) { elementStack.add(this.elementTags[i]); } } return Collections.unmodifiableList(elementStack); } public List<IProcessableElementTag> getElementStackAbove(final int contextLevel) { final List<IProcessableElementTag> elementStack = new ArrayList<IProcessableElementTag>(this.level); for (int i = contextLevel + 1; i <= this.level && i < this.elementTags.length; i++) { if (this.elementTags[i] != null) { elementStack.add(this.elementTags[i]); } } return Collections.unmodifiableList(elementStack); } private void ensureLevelInitialized(final boolean initVariables) { // First, check if the current index already signals the current level (in which case, everything is OK) if (this.levels[this.index] != this.level) { // The current level still had no index assigned -- we must do it, and maybe even grow structures this.index++; // This new index will be the one for our level if (this.levels.length == this.index) { this.levels = Arrays.copyOf(this.levels, this.levels.length + DEFAULT_LEVELS_SIZE); Arrays.fill(this.levels, this.index, this.levels.length, Integer.MAX_VALUE); // We fill the new places with MAX_VALUE this.names = Arrays.copyOf(this.names, this.names.length + DEFAULT_LEVELS_SIZE); this.newValues = Arrays.copyOf(this.newValues, this.newValues.length + DEFAULT_LEVELS_SIZE); this.oldValues = Arrays.copyOf(this.oldValues, this.oldValues.length + DEFAULT_LEVELS_SIZE); this.levelSizes = Arrays.copyOf(this.levelSizes, this.levelSizes.length + DEFAULT_LEVELS_SIZE); // No need to initialize new places in this.levelSizes as copyOf already fills with zeroes this.selectionTargets = Arrays.copyOf(this.selectionTargets, this.selectionTargets.length + DEFAULT_LEVELS_SIZE); this.inliners = Arrays.copyOf(this.inliners, this.inliners.length + DEFAULT_LEVELS_SIZE); this.templateDatas = Arrays.copyOf(this.templateDatas, this.templateDatas.length + DEFAULT_LEVELS_SIZE); } this.levels[this.index] = this.level; } if (this.level > 0) { // We will only take care of new/old values if we are not on level 0 if (initVariables && this.names[this.index] == null) { // the arrays for this level have still not been created this.names[this.index] = new String[DEFAULT_LEVELARRAYS_SIZE]; Arrays.fill(this.names[this.index], null); this.newValues[this.index] = new Object[DEFAULT_LEVELARRAYS_SIZE]; Arrays.fill(this.newValues[this.index], null); this.oldValues[this.index] = new Object[DEFAULT_LEVELARRAYS_SIZE]; Arrays.fill(this.oldValues[this.index], null); this.levelSizes[this.index] = 0; } } } public int level() { return this.level; } public void increaseLevel() { this.level++; } public void decreaseLevel() { Validate.isTrue(this.level > 0, "Cannot decrease variable map level below 0"); if (this.levels[this.index] == this.level) { this.levels[this.index] = Integer.MAX_VALUE; if (this.names[this.index] != null && this.levelSizes[this.index] > 0) { // There were movements at this level, so we have to revert them int n = this.levelSizes[this.index]; while (n-- != 0) { final String name = this.names[this.index][n]; final Object newValue = this.newValues[this.index][n]; final Object oldValue = this.oldValues[this.index][n]; final Object currentValue = this.request.getAttribute(name); if (newValue == currentValue) { // Only if the value matches, in order to avoid modifying values that have been set directly // into the request. this.request.setAttribute(name,oldValue); } } this.levelSizes[this.index] = 0; } this.selectionTargets[this.index] = null; this.inliners[this.index] = null; this.templateDatas[this.index] = null; this.index--; // These might not belong to this level, but just in case... this.lastSelectionTarget = null; this.lastInliner = null; this.lastTemplateData = null; this.templateStack.clear(); } if (this.level < this.elementTags.length) { this.elementTags[this.level] = null; } this.level--; } public String getStringRepresentationByLevel() { final StringBuilder strBuilder = new StringBuilder(); strBuilder.append('{'); final Map<String,Object> oldValuesSum = new LinkedHashMap<String, Object>(); int n = this.index + 1; while (n-- != 1) { final Map<String,Object> levelVars = new LinkedHashMap<String, Object>(); if (this.names[n] != null && this.levelSizes[n] > 0) { for (int i = 0; i < this.levelSizes[n]; i++) { final String name = this.names[n][i]; final Object newValue = this.newValues[n][i]; final Object oldValue = this.oldValues[n][i]; if (newValue == oldValue) { // This is a no-op! continue; } if (!oldValuesSum.containsKey(name)) { // This means that, either the value in the request is the same as the newValue, or it was modified // directly at the request and we need to discard this entry. if (newValue != this.request.getAttribute(name)) { continue; } } else { // This means that, either the old value in the map is the same as the newValue, or it was modified // directly at the request and we need to discard this entry. if (newValue != oldValuesSum.get(name)) { continue; } } levelVars.put(name, newValue); oldValuesSum.put(name, oldValue); } } if (!levelVars.isEmpty() || this.selectionTargets[n] != null || this.inliners[n] != null) { if (strBuilder.length() > 1) { strBuilder.append(','); } strBuilder.append(this.levels[n]).append(":"); if (!levelVars.isEmpty() || n == 0) { strBuilder.append(levelVars); } if (this.selectionTargets[n] != null) { strBuilder.append("<").append(this.selectionTargets[n].selectionTarget).append(">"); } if (this.inliners[n] != null) { strBuilder.append("[").append(this.inliners[n].getName()).append("]"); } if (this.templateDatas[n] != null) { strBuilder.append("(").append(this.templateDatas[n].getTemplate()).append(")"); } } } final Map<String,Object> requestAttributes = new LinkedHashMap<String, Object>(); final Enumeration<String> attrNames = this.request.getAttributeNames(); while (attrNames.hasMoreElements()) { final String name = attrNames.nextElement(); if (oldValuesSum.containsKey(name)) { final Object oldValue = oldValuesSum.get(name); if (oldValue != null) { requestAttributes.put(name, oldValuesSum.get(name)); } oldValuesSum.remove(name); } else { requestAttributes.put(name, this.request.getAttribute(name)); } } for (Map.Entry<String,Object> oldValuesSumEntry : oldValuesSum.entrySet()) { final String name = oldValuesSumEntry.getKey(); if (!requestAttributes.containsKey(name)) { final Object oldValue = oldValuesSumEntry.getValue(); if (oldValue != null) { requestAttributes.put(name, oldValue); } } } if (strBuilder.length() > 1) { strBuilder.append(','); } strBuilder.append(this.levels[n]).append(":"); strBuilder.append(requestAttributes.toString()); if (this.selectionTargets[0] != null) { strBuilder.append("<").append(this.selectionTargets[0].selectionTarget).append(">"); } if (this.inliners[0] != null) { strBuilder.append("[").append(this.inliners[0].getName()).append("]"); } if (this.templateDatas[0] != null) { strBuilder.append("(").append(this.templateDatas[0].getTemplate()).append(")"); } strBuilder.append("}["); strBuilder.append(this.level); strBuilder.append(']'); return strBuilder.toString(); } @Override public String toString() { final Map<String,Object> equivalentMap = new LinkedHashMap<String, Object>(); final Enumeration<String> attributeNamesEnum = this.request.getAttributeNames(); while (attributeNamesEnum.hasMoreElements()) { final String name = attributeNamesEnum.nextElement(); equivalentMap.put(name, this.request.getAttribute(name)); } final String textInliningStr = (getInliner() != null? "[" + getInliner().getName() + "]" : "" ); final String templateDataStr = "(" + getTemplateData().getTemplate() + ")"; return equivalentMap.toString() + (hasSelectionTarget()? "<" + getSelectionTarget() + ">" : "") + textInliningStr + templateDataStr; } /* * This class works as a wrapper for the selection target, in order to differentiate whether we * have set a selection target, we have not, or we have set it but it's null */ private static final class SelectionTarget { final Object selectionTarget; SelectionTarget(final Object selectionTarget) { super(); this.selectionTarget = selectionTarget; } } } private abstract static class NoOpMapImpl implements Map<String,Object> { protected NoOpMapImpl() { super(); } public int size() { return 0; } public boolean isEmpty() { return true; } public boolean containsKey(final Object key) { return false; } public boolean containsValue(final Object value) { return false; } public Object get(final Object key) { return null; } public Object put(final String key, final Object value) { throw new UnsupportedOperationException("Cannot add new entry: map is immutable"); } public Object remove(final Object key) { throw new UnsupportedOperationException("Cannot remove entry: map is immutable"); } public void putAll(final Map<? extends String, ? extends Object> m) { throw new UnsupportedOperationException("Cannot add new entry: map is immutable"); } public void clear() { throw new UnsupportedOperationException("Cannot clear: map is immutable"); } public Set<String> keySet() { return Collections.emptySet(); } public Collection<Object> values() { return Collections.emptyList(); } public Set<Entry<String,Object>> entrySet() { return Collections.emptySet(); } static final class MapEntry implements Map.Entry<String,Object> { private final String key; private final Object value; MapEntry(final String key, final Object value) { super(); this.key = key; this.value = value; } public String getKey() { return this.key; } public Object getValue() { return this.value; } public Object setValue(final Object value) { throw new UnsupportedOperationException("Cannot set value: map is immutable"); } } } private static final class RequestParameterValues extends AbstractList<String> { private final String[] parameterValues; public final int length; RequestParameterValues(final String[] parameterValues) { this.parameterValues = parameterValues; this.length = this.parameterValues.length; } @Override public int size() { return this.length; } @Override public Object[] toArray() { return this.parameterValues.clone(); } @Override public <T> T[] toArray(final T[] arr) { if (arr.length < this.length) { final T[] copy = (T[]) Array.newInstance(arr.getClass().getComponentType(), this.length); System.arraycopy(this.parameterValues, 0, copy, 0, this.length); return copy; } System.arraycopy(this.parameterValues, 0, arr, 0, this.length); if (arr.length > this.length) { arr[this.length] = null; } return arr; } @Override public String get(final int index) { return this.parameterValues[index]; } @Override public int indexOf(final Object obj) { final String[] a = this.parameterValues; if (obj == null) { for (int i = 0; i < a.length; i++) { if (a[i] == null) { return i; } } } else { for (int i = 0; i < a.length; i++) { if (obj.equals(a[i])) { return i; } } } return -1; } @Override public boolean contains(final Object obj) { return indexOf(obj) != -1; } @Override public String toString() { // This toString() method will be responsible of outputting non-indexed request parameters in the // way most people expect, i.e. return parameterValues[0] when accessed without index and parameter is // single-valued (${param.a}), returning ArrayList#toString() when accessed without index and parameter // is multi-valued, and finally return the specific value when accessed with index (${param.a[0]}) if (this.length == 0) { return ""; } if (this.length == 1) { return this.parameterValues[0]; } return super.toString(); } } }