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.form;
018
019import java.io.IOException;
020import java.io.Writer;
021import java.util.Stack;
022import javax.servlet.jsp.JspException;
023import javax.servlet.jsp.PageContext;
024
025import org.springframework.util.Assert;
026import org.springframework.util.StringUtils;
027
028/**
029 * Utility class for writing HTML content to a {@link Writer} instance.
030 *
031 * <p>Intended to support output from JSP tag libraries.
032 *
033 * @author Rob Harrop
034 * @author Juergen Hoeller
035 * @since 2.0
036 */
037public class TagWriter {
038
039        /**
040         * The {@link SafeWriter} to write to.
041         */
042        private final SafeWriter writer;
043
044        /**
045         * Stores {@link TagStateEntry tag state}. Stack model naturally supports tag nesting.
046         */
047        private final Stack<TagStateEntry> tagState = new Stack<TagStateEntry>();
048
049
050        /**
051         * Create a new instance of the {@link TagWriter} class that writes to
052         * the supplied {@link PageContext}.
053         * @param pageContext the JSP PageContext to obtain the {@link Writer} from
054         */
055        public TagWriter(PageContext pageContext) {
056                Assert.notNull(pageContext, "PageContext must not be null");
057                this.writer = new SafeWriter(pageContext);
058        }
059
060        /**
061         * Create a new instance of the {@link TagWriter} class that writes to
062         * the supplied {@link Writer}.
063         * @param writer the {@link Writer} to write tag content to
064         */
065        public TagWriter(Writer writer) {
066                Assert.notNull(writer, "Writer must not be null");
067                this.writer = new SafeWriter(writer);
068        }
069
070
071        /**
072         * Start a new tag with the supplied name. Leaves the tag open so
073         * that attributes, inner text or nested tags can be written into it.
074         * @see #endTag()
075         */
076        public void startTag(String tagName) throws JspException {
077                if (inTag()) {
078                        closeTagAndMarkAsBlock();
079                }
080                push(tagName);
081                this.writer.append("<").append(tagName);
082        }
083
084        /**
085         * Write an HTML attribute with the specified name and value.
086         * <p>Be sure to write all attributes <strong>before</strong> writing
087         * any inner text or nested tags.
088         * @throws IllegalStateException if the opening tag is closed
089         */
090        public void writeAttribute(String attributeName, String attributeValue) throws JspException {
091                if (currentState().isBlockTag()) {
092                        throw new IllegalStateException("Cannot write attributes after opening tag is closed.");
093                }
094                this.writer.append(" ").append(attributeName).append("=\"")
095                                .append(attributeValue).append("\"");
096        }
097
098        /**
099         * Write an HTML attribute if the supplied value is not {@code null}
100         * or zero length.
101         * @see #writeAttribute(String, String)
102         */
103        public void writeOptionalAttributeValue(String attributeName, String attributeValue) throws JspException {
104                if (StringUtils.hasText(attributeValue)) {
105                        writeAttribute(attributeName, attributeValue);
106                }
107        }
108
109        /**
110         * Close the current opening tag (if necessary) and appends the
111         * supplied value as inner text.
112         * @throws IllegalStateException if no tag is open
113         */
114        public void appendValue(String value) throws JspException {
115                if (!inTag()) {
116                        throw new IllegalStateException("Cannot write tag value. No open tag available.");
117                }
118                closeTagAndMarkAsBlock();
119                this.writer.append(value);
120        }
121
122
123        /**
124         * Indicate that the currently open tag should be closed and marked
125         * as a block level element.
126         * <p>Useful when you plan to write additional content in the body
127         * outside the context of the current {@link TagWriter}.
128         */
129        public void forceBlock() throws JspException {
130                if (currentState().isBlockTag()) {
131                        return; // just ignore since we are already in the block
132                }
133                closeTagAndMarkAsBlock();
134        }
135
136        /**
137         * Close the current tag.
138         * <p>Correctly writes an empty tag if no inner text or nested tags
139         * have been written.
140         */
141        public void endTag() throws JspException {
142                endTag(false);
143        }
144
145        /**
146         * Close the current tag, allowing to enforce a full closing tag.
147         * <p>Correctly writes an empty tag if no inner text or nested tags
148         * have been written.
149         * @param enforceClosingTag whether a full closing tag should be
150         * rendered in any case, even in case of a non-block tag
151         */
152        public void endTag(boolean enforceClosingTag) throws JspException {
153                if (!inTag()) {
154                        throw new IllegalStateException("Cannot write end of tag. No open tag available.");
155                }
156                boolean renderClosingTag = true;
157                if (!currentState().isBlockTag()) {
158                        // Opening tag still needs to be closed...
159                        if (enforceClosingTag) {
160                                this.writer.append(">");
161                        }
162                        else {
163                                this.writer.append("/>");
164                                renderClosingTag = false;
165                        }
166                }
167                if (renderClosingTag) {
168                        this.writer.append("</").append(currentState().getTagName()).append(">");
169                }
170                this.tagState.pop();
171        }
172
173
174        /**
175         * Adds the supplied tag name to the {@link #tagState tag state}.
176         */
177        private void push(String tagName) {
178                this.tagState.push(new TagStateEntry(tagName));
179        }
180
181        /**
182         * Closes the current opening tag and marks it as a block tag.
183         */
184        private void closeTagAndMarkAsBlock() throws JspException {
185                if (!currentState().isBlockTag()) {
186                        currentState().markAsBlockTag();
187                        this.writer.append(">");
188                }
189        }
190
191        private boolean inTag() {
192                return !this.tagState.isEmpty();
193        }
194
195        private TagStateEntry currentState() {
196                return this.tagState.peek();
197        }
198
199
200        /**
201         * Holds state about a tag and its rendered behavior.
202         */
203        private static class TagStateEntry {
204
205                private final String tagName;
206
207                private boolean blockTag;
208
209                public TagStateEntry(String tagName) {
210                        this.tagName = tagName;
211                }
212
213                public String getTagName() {
214                        return this.tagName;
215                }
216
217                public void markAsBlockTag() {
218                        this.blockTag = true;
219                }
220
221                public boolean isBlockTag() {
222                        return this.blockTag;
223                }
224        }
225
226
227        /**
228         * Simple {@link Writer} wrapper that wraps all
229         * {@link IOException IOExceptions} in {@link JspException JspExceptions}.
230         */
231        private static final class SafeWriter {
232
233                private PageContext pageContext;
234
235                private Writer writer;
236
237                public SafeWriter(PageContext pageContext) {
238                        this.pageContext = pageContext;
239                }
240
241                public SafeWriter(Writer writer) {
242                        this.writer = writer;
243                }
244
245                public SafeWriter append(String value) throws JspException {
246                        try {
247                                getWriterToUse().write(String.valueOf(value));
248                                return this;
249                        }
250                        catch (IOException ex) {
251                                throw new JspException("Unable to write to JspWriter", ex);
252                        }
253                }
254
255                private Writer getWriterToUse() {
256                        return (this.pageContext != null ? this.pageContext.getOut() : this.writer);
257                }
258        }
259
260}