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