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