Open Source Repository

Home /spring/spring-web-servlet-3.0.5 | Repository Home



org/springframework/web/servlet/tags/form/TagWriter.java
/*
 * Copyright 2002-2008 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
 *
 *      http://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.Stack;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;

import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Utility class for writing HTML content to a {@link Writer} instance.
 *
 <p>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 Stack tagState = new Stack();


  /**
   * 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 tagNamethrows JspException {
    if (inTag()) {
      closeTagAndMarkAsBlock();
    }
    push(tagName);
    this.writer.append("<").append(tagName);
  }

  /**
   * Write an HTML attribute with the specified name and value.
   <p>Be sure to write all attributes <strong>before</strong> writing
   * any inner text or nested tags.
   @throws IllegalStateException if the opening tag is closed
   */
  public void writeAttribute(String attributeName, String attributeValuethrows JspException {
    if (currentState().isBlockTag()) {
      throw new IllegalStateException("Cannot write attributes after opening tag is closed.");
    }
    this.writer.append(" ").append(attributeName).append("=\"")
        .append(attributeValue).append("\"");
  }

  /**
   * Write an HTML attribute if the supplied value is not <code>null</code>
   * or zero length.
   @see #writeAttribute(String, String)
   */
  public void writeOptionalAttributeValue(String attributeName, String attributeValuethrows 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 valuethrows 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.
   <p>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.
   <p>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.
   <p>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 enforceClosingTagthrows 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("</").append(currentState().getTagName()).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.size() 0;
  }

  private TagStateEntry currentState() {
    return (TagStateEntrythis.tagState.peek();
  }


  /**
   * 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 {

    private PageContext pageContext;

    private Writer writer;

    public SafeWriter(PageContext pageContext) {
      this.pageContext = pageContext;
    }

    public SafeWriter(Writer writer) {
      this.writer = writer;
    }

    public SafeWriter append(String valuethrows JspException {
      try {
        getWriterToUse().write(String.valueOf(value));
        return this;
      }
      catch (IOException ex) {
        throw new JspException("Unable to write to JspWriter", ex);
      }
    }

    private Writer getWriterToUse() {
      return (this.pageContext != null this.pageContext.getOut() this.writer);
    }
  }

}