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}