1 /*
2  * Copyright 2002-2008 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.web.servlet.tags.form;
18 
19 import java.io.IOException;
20 import java.io.Writer;
21 import java.util.Stack;
22 
23 import javax.servlet.jsp.JspException;
24 import javax.servlet.jsp.PageContext;
25 
26 import org.springframework.util.Assert;
27 import org.springframework.util.StringUtils;
28 
29 /**
30  * Utility class for writing HTML content to a {@link Writer} instance.
31  *
32  * <p>Intended to support output from JSP tag libraries.
33  *
34  * @author Rob Harrop
35  * @author Juergen Hoeller
36  * @since 2.0
37  */
38 public class TagWriter {
39 
40 	/**
41 	 * The {@link SafeWriter} to write to.
42 	 */
43 	private final SafeWriter writer;
44 
45 	/**
46 	 * Stores {@link TagStateEntry tag state}. Stack model naturally supports tag nesting.
47 	 */
48 	private final Stack tagState = new Stack();
49 
50 
51 	/**
52 	 * Create a new instance of the {@link TagWriter} class that writes to
53 	 * the supplied {@link PageContext}.
54 	 * @param pageContext the JSP PageContext to obtain the {@link Writer} from
55 	 */
TagWriter(PageContext pageContext)56 	public TagWriter(PageContext pageContext) {
57 		Assert.notNull(pageContext, "PageContext must not be null");
58 		this.writer = new SafeWriter(pageContext);
59 	}
60 
61 	/**
62 	 * Create a new instance of the {@link TagWriter} class that writes to
63 	 * the supplied {@link Writer}.
64 	 * @param writer the {@link Writer} to write tag content to
65 	 */
TagWriter(Writer writer)66 	public TagWriter(Writer writer) {
67 		Assert.notNull(writer, "Writer must not be null");
68 		this.writer = new SafeWriter(writer);
69 	}
70 
71 
72 	/**
73 	 * Start a new tag with the supplied name. Leaves the tag open so
74 	 * that attributes, inner text or nested tags can be written into it.
75 	 * @see #endTag()
76 	 */
startTag(String tagName)77 	public void startTag(String tagName) throws JspException {
78 		if (inTag()) {
79 			closeTagAndMarkAsBlock();
80 		}
81 		push(tagName);
82 		this.writer.append("<").append(tagName);
83 	}
84 
85 	/**
86 	 * Write an HTML attribute with the specified name and value.
87 	 * <p>Be sure to write all attributes <strong>before</strong> writing
88 	 * any inner text or nested tags.
89 	 * @throws IllegalStateException if the opening tag is closed
90 	 */
writeAttribute(String attributeName, String attributeValue)91 	public void writeAttribute(String attributeName, String attributeValue) throws JspException {
92 		if (currentState().isBlockTag()) {
93 			throw new IllegalStateException("Cannot write attributes after opening tag is closed.");
94 		}
95 		this.writer.append(" ").append(attributeName).append("=\"")
96 				.append(attributeValue).append("\"");
97 	}
98 
99 	/**
100 	 * Write an HTML attribute if the supplied value is not <code>null</code>
101 	 * or zero length.
102 	 * @see #writeAttribute(String, String)
103 	 */
writeOptionalAttributeValue(String attributeName, String attributeValue)104 	public void writeOptionalAttributeValue(String attributeName, String attributeValue) throws JspException {
105 		if (StringUtils.hasText(attributeValue)) {
106 			writeAttribute(attributeName, attributeValue);
107 		}
108 	}
109 
110 	/**
111 	 * Close the current opening tag (if necessary) and appends the
112 	 * supplied value as inner text.
113 	 * @throws IllegalStateException if no tag is open
114 	 */
appendValue(String value)115 	public void appendValue(String value) throws JspException {
116 		if (!inTag()) {
117 			throw new IllegalStateException("Cannot write tag value. No open tag available.");
118 		}
119 		closeTagAndMarkAsBlock();
120 		this.writer.append(value);
121 	}
122 
123 
124 	/**
125 	 * Indicate that the currently open tag should be closed and marked
126 	 * as a block level element.
127 	 * <p>Useful when you plan to write additional content in the body
128 	 * outside the context of the current {@link TagWriter}.
129 	 */
forceBlock()130 	public void forceBlock() throws JspException {
131 		if (currentState().isBlockTag()) {
132 			return; // just ignore since we are already in the block
133 		}
134 		closeTagAndMarkAsBlock();
135 	}
136 
137 	/**
138 	 * Close the current tag.
139 	 * <p>Correctly writes an empty tag if no inner text or nested tags
140 	 * have been written.
141 	 */
endTag()142 	public void endTag() throws JspException {
143 		endTag(false);
144 	}
145 
146 	/**
147 	 * Close the current tag, allowing to enforce a full closing tag.
148 	 * <p>Correctly writes an empty tag if no inner text or nested tags
149 	 * have been written.
150 	 * @param enforceClosingTag whether a full closing tag should be
151 	 * rendered in any case, even in case of a non-block tag
152 	 */
endTag(boolean enforceClosingTag)153 	public void endTag(boolean enforceClosingTag) throws JspException {
154 		if (!inTag()) {
155 			throw new IllegalStateException("Cannot write end of tag. No open tag available.");
156 		}
157 		boolean renderClosingTag = true;
158 		if (!currentState().isBlockTag()) {
159 			// Opening tag still needs to be closed...
160 			if (enforceClosingTag) {
161 				this.writer.append(">");
162 			}
163 			else {
164 				this.writer.append("/>");
165 				renderClosingTag = false;
166 			}
167 		}
168 		if (renderClosingTag) {
169 			this.writer.append("</").append(currentState().getTagName()).append(">");
170 		}
171 		this.tagState.pop();
172 	}
173 
174 
175 	/**
176 	 * Adds the supplied tag name to the {@link #tagState tag state}.
177 	 */
push(String tagName)178 	private void push(String tagName) {
179 		this.tagState.push(new TagStateEntry(tagName));
180 	}
181 
182 	/**
183 	 * Closes the current opening tag and marks it as a block tag.
184 	 */
closeTagAndMarkAsBlock()185 	private void closeTagAndMarkAsBlock() throws JspException {
186 		if (!currentState().isBlockTag()) {
187 			currentState().markAsBlockTag();
188 			this.writer.append(">");
189 		}
190 	}
191 
inTag()192 	private boolean inTag() {
193 		return this.tagState.size() > 0;
194 	}
195 
currentState()196 	private TagStateEntry currentState() {
197 		return (TagStateEntry) this.tagState.peek();
198 	}
199 
200 
201 	/**
202 	 * Holds state about a tag and its rendered behavior.
203 	 */
204 	private static class TagStateEntry {
205 
206 		private final String tagName;
207 
208 		private boolean blockTag;
209 
TagStateEntry(String tagName)210 		public TagStateEntry(String tagName) {
211 			this.tagName = tagName;
212 		}
213 
getTagName()214 		public String getTagName() {
215 			return this.tagName;
216 		}
217 
markAsBlockTag()218 		public void markAsBlockTag() {
219 			this.blockTag = true;
220 		}
221 
isBlockTag()222 		public boolean isBlockTag() {
223 			return this.blockTag;
224 		}
225 	}
226 
227 
228 	/**
229 	 * Simple {@link Writer} wrapper that wraps all
230 	 * {@link IOException IOExceptions} in {@link JspException JspExceptions}.
231 	 */
232 	private static final class SafeWriter {
233 
234 		private PageContext pageContext;
235 
236 		private Writer writer;
237 
SafeWriter(PageContext pageContext)238 		public SafeWriter(PageContext pageContext) {
239 			this.pageContext = pageContext;
240 		}
241 
SafeWriter(Writer writer)242 		public SafeWriter(Writer writer) {
243 			this.writer = writer;
244 		}
245 
append(String value)246 		public SafeWriter append(String value) throws JspException {
247 			try {
248 				getWriterToUse().write(String.valueOf(value));
249 				return this;
250 			}
251 			catch (IOException ex) {
252 				throw new JspException("Unable to write to JspWriter", ex);
253 			}
254 		}
255 
getWriterToUse()256 		private Writer getWriterToUse() {
257 			return (this.pageContext != null ? this.pageContext.getOut() : this.writer);
258 		}
259 	}
260 
261 }
262