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}