001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.tags;
018
019import javax.servlet.jsp.JspException;
020import javax.servlet.jsp.PageContext;
021import javax.servlet.jsp.tagext.TagSupport;
022import javax.servlet.jsp.tagext.TryCatchFinally;
023
024import org.springframework.beans.PropertyAccessor;
025import org.springframework.lang.Nullable;
026
027/**
028 * <p>The {@code <nestedPath>} tag supports and assists with nested beans or
029 * bean properties in the model. Exports a "nestedPath" variable of type String
030 * in request scope, visible to the current page and also included pages, if any.
031 *
032 * <p>The BindTag will auto-detect the current nested path and automatically
033 * prepend it to its own path to form a complete path to the bean or bean property.
034 *
035 * <p>This tag will also prepend any existing nested path that is currently set.
036 * Thus, you can nest multiple nested-path tags.
037 *
038 * <table>
039 * <caption>Attribute Summary</caption>
040 * <thead>
041 * <tr>
042 * <th>Attribute</th>
043 * <th>Required?</th>
044 * <th>Runtime Expression?</th>
045 * <th>Description</th>
046 * </tr>
047 * </thead>
048 * <tbody>
049 * <tr>
050 * <td>path</td>
051 * <td>true</td>
052 * <td>true</td>
053 * <td>Set the path that this tag should apply. E.g. 'customer' to allow bind
054 * paths like 'address.street' rather than 'customer.address.street'.</td>
055 * </tr>
056 * </tbody>
057 * </table>
058 *
059 * @author Juergen Hoeller
060 * @since 1.1
061 */
062@SuppressWarnings("serial")
063public class NestedPathTag extends TagSupport implements TryCatchFinally {
064
065        /**
066         * Name of the exposed variable within the scope of this tag: "nestedPath".
067         */
068        public static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
069
070
071        @Nullable
072        private String path;
073
074        /** Caching a previous nested path, so that it may be reset. */
075        @Nullable
076        private String previousNestedPath;
077
078
079        /**
080         * Set the path that this tag should apply.
081         * <p>E.g. "customer" to allow bind paths like "address.street"
082         * rather than "customer.address.street".
083         * @see BindTag#setPath
084         */
085        public void setPath(@Nullable String path) {
086                if (path == null) {
087                        path = "";
088                }
089                if (path.length() > 0 && !path.endsWith(PropertyAccessor.NESTED_PROPERTY_SEPARATOR)) {
090                        path += PropertyAccessor.NESTED_PROPERTY_SEPARATOR;
091                }
092                this.path = path;
093        }
094
095        /**
096         * Return the path that this tag applies to.
097         */
098        @Nullable
099        public String getPath() {
100                return this.path;
101        }
102
103
104        @Override
105        public int doStartTag() throws JspException {
106                // Save previous nestedPath value, build and expose current nestedPath value.
107                // Use request scope to expose nestedPath to included pages too.
108                this.previousNestedPath =
109                                (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
110                String nestedPath =
111                                (this.previousNestedPath != null ? this.previousNestedPath + getPath() : getPath());
112                this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, nestedPath, PageContext.REQUEST_SCOPE);
113
114                return EVAL_BODY_INCLUDE;
115        }
116
117        /**
118         * Reset any previous nestedPath value.
119         */
120        @Override
121        public int doEndTag() {
122                if (this.previousNestedPath != null) {
123                        // Expose previous nestedPath value.
124                        this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE);
125                }
126                else {
127                        // Remove exposed nestedPath value.
128                        this.pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
129                }
130
131                return EVAL_PAGE;
132        }
133
134        @Override
135        public void doCatch(Throwable throwable) throws Throwable {
136                throw throwable;
137        }
138
139        @Override
140        public void doFinally() {
141                this.previousNestedPath = null;
142        }
143
144}