org.springframework.web.servlet.tags.form.TagWriter Maven / Gradle / Ivy
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.tags.form;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.Deque;
import jakarta.servlet.jsp.JspException;
import jakarta.servlet.jsp.PageContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Utility class for writing HTML content to a {@link Writer} instance.
*
* Intended to support output from JSP tag libraries.
*
* @author Rob Harrop
* @author Juergen Hoeller
* @since 2.0
*/
public class TagWriter {
/**
* The {@link SafeWriter} to write to.
*/
private final SafeWriter writer;
/**
* Stores {@link TagStateEntry tag state}. Stack model naturally supports tag nesting.
*/
private final Deque tagState = new ArrayDeque<>();
/**
* Create a new instance of the {@link TagWriter} class that writes to
* the supplied {@link PageContext}.
* @param pageContext the JSP PageContext to obtain the {@link Writer} from
*/
public TagWriter(PageContext pageContext) {
Assert.notNull(pageContext, "PageContext must not be null");
this.writer = new SafeWriter(pageContext);
}
/**
* Create a new instance of the {@link TagWriter} class that writes to
* the supplied {@link Writer}.
* @param writer the {@link Writer} to write tag content to
*/
public TagWriter(Writer writer) {
Assert.notNull(writer, "Writer must not be null");
this.writer = new SafeWriter(writer);
}
/**
* Start a new tag with the supplied name. Leaves the tag open so
* that attributes, inner text or nested tags can be written into it.
* @see #endTag()
*/
public void startTag(String tagName) throws JspException {
if (inTag()) {
closeTagAndMarkAsBlock();
}
push(tagName);
this.writer.append("<").append(tagName);
}
/**
* Write an HTML attribute with the specified name and value.
* Be sure to write all attributes before writing
* any inner text or nested tags.
* @throws IllegalStateException if the opening tag is closed
*/
public void writeAttribute(String attributeName, String attributeValue) throws JspException {
if (currentState().isBlockTag()) {
throw new IllegalStateException("Cannot write attributes after opening tag is closed.");
}
this.writer.append(" ").append(attributeName).append("=\"")
.append(attributeValue).append("\"");
}
/**
* Variant of {@link #writeAttribute(String, String)} for writing empty HTML
* attributes without a value such as {@code required}.
* @since 5.3.14
*/
public void writeAttribute(String attributeName) throws JspException {
if (currentState().isBlockTag()) {
throw new IllegalStateException("Cannot write attributes after opening tag is closed.");
}
this.writer.append(" ").append(attributeName);
}
/**
* Write an HTML attribute if the supplied value is not {@code null}
* or zero length.
* @see #writeAttribute(String, String)
*/
public void writeOptionalAttributeValue(String attributeName, @Nullable String attributeValue) throws JspException {
if (StringUtils.hasText(attributeValue)) {
writeAttribute(attributeName, attributeValue);
}
}
/**
* Close the current opening tag (if necessary) and appends the
* supplied value as inner text.
* @throws IllegalStateException if no tag is open
*/
public void appendValue(String value) throws JspException {
if (!inTag()) {
throw new IllegalStateException("Cannot write tag value. No open tag available.");
}
closeTagAndMarkAsBlock();
this.writer.append(value);
}
/**
* Indicate that the currently open tag should be closed and marked
* as a block level element.
*
Useful when you plan to write additional content in the body
* outside the context of the current {@link TagWriter}.
*/
public void forceBlock() throws JspException {
if (currentState().isBlockTag()) {
return; // just ignore since we are already in the block
}
closeTagAndMarkAsBlock();
}
/**
* Close the current tag.
*
Correctly writes an empty tag if no inner text or nested tags
* have been written.
*/
public void endTag() throws JspException {
endTag(false);
}
/**
* Close the current tag, allowing to enforce a full closing tag.
*
Correctly writes an empty tag if no inner text or nested tags
* have been written.
* @param enforceClosingTag whether a full closing tag should be
* rendered in any case, even in case of a non-block tag
*/
public void endTag(boolean enforceClosingTag) throws JspException {
if (!inTag()) {
throw new IllegalStateException("Cannot write end of tag. No open tag available.");
}
boolean renderClosingTag = true;
if (!currentState().isBlockTag()) {
// Opening tag still needs to be closed...
if (enforceClosingTag) {
this.writer.append(">");
}
else {
this.writer.append("/>");
renderClosingTag = false;
}
}
if (renderClosingTag) {
this.writer.append("");
}
this.tagState.pop();
}
/**
* Adds the supplied tag name to the {@link #tagState tag state}.
*/
private void push(String tagName) {
this.tagState.push(new TagStateEntry(tagName));
}
/**
* Closes the current opening tag and marks it as a block tag.
*/
private void closeTagAndMarkAsBlock() throws JspException {
if (!currentState().isBlockTag()) {
currentState().markAsBlockTag();
this.writer.append(">");
}
}
private boolean inTag() {
return !this.tagState.isEmpty();
}
private TagStateEntry currentState() {
return this.tagState.element();
}
/**
* Holds state about a tag and its rendered behavior.
*/
private static class TagStateEntry {
private final String tagName;
private boolean blockTag;
public TagStateEntry(String tagName) {
this.tagName = tagName;
}
public String getTagName() {
return this.tagName;
}
public void markAsBlockTag() {
this.blockTag = true;
}
public boolean isBlockTag() {
return this.blockTag;
}
}
/**
* Simple {@link Writer} wrapper that wraps all
* {@link IOException IOExceptions} in {@link JspException JspExceptions}.
*/
private static final class SafeWriter {
@Nullable
private PageContext pageContext;
@Nullable
private Writer writer;
public SafeWriter(PageContext pageContext) {
this.pageContext = pageContext;
}
public SafeWriter(Writer writer) {
this.writer = writer;
}
public SafeWriter append(String value) throws JspException {
try {
getWriterToUse().write(String.valueOf(value));
return this;
}
catch (IOException ex) {
throw new JspException("Unable to write to JspWriter", ex);
}
}
private Writer getWriterToUse() {
Writer writer = (this.pageContext != null ? this.pageContext.getOut() : this.writer);
Assert.state(writer != null, "No Writer available");
return writer;
}
}
}