| // Copyright 2004 The Apache Software Foundation |
| // |
| // 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.apache.tapestry; |
| |
| import java.io.BufferedWriter; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.io.Writer; |
| import java.util.Stack; |
| |
| import org.apache.tapestry.util.ContentType; |
| |
| /** |
| * Abstract base class implementing the {@link IMarkupWriter} interface. |
| * This class is used to create a Generic Tag Markup Language (GTML) output. |
| * It is more sophisticated than <code>PrintWriter</code> in that it maintains |
| * a concept hierarchy of open GTML tags. It also supplies a number of other |
| * of the features that are useful when creating GTML. |
| * |
| * Elements are started with the {@link #begin(String)} |
| * or {@link #beginEmpty(String)} |
| * methods. Once they are started, attributes for the elements may be set with |
| * the various <code>attribute()</code> methods. The element is closed off |
| * (i.e., the closing '>' character is written) when any other method |
| * is invoked (exception: methods which do not produce output, such as |
| * {@link #flush()}). The <code>end()</code> methods end an element, |
| * writing an GTML close tag to the output. |
| * |
| * <p>TBD: |
| * <ul> |
| * <li>Support XML and XHTML |
| * <li>What to do with Unicode characters with a value greater than 255? |
| * </ul> |
| * |
| * <p>This class is derived from the original class |
| * <code>com.primix.servlet.HTMLWriter</code>, |
| * part of the <b>ServletUtils</b> framework available from |
| * <a href="http://www.gjt.org/servlets/JCVSlet/list/gjt/com/primix/servlet">The Giant |
| * Java Tree</a>. |
| * |
| * @version $Id$ |
| * @author Howard Ship, David Solis |
| * @since 0.2.9 |
| * |
| **/ |
| |
| public abstract class AbstractMarkupWriter implements IMarkupWriter |
| { |
| /** |
| * The encoding to be used should it be omitted in the constructors. |
| * This is only used for backward compatibility. New code always provides the encoding. |
| */ |
| |
| private static final String DEFAULT_ENCODING = "utf-8"; |
| |
| /** |
| * The underlying {@link PrintWriter} that output is sent to. |
| * |
| **/ |
| |
| private PrintWriter _writer; |
| |
| /** |
| * Indicates whether a tag is open or not. A tag is opened by |
| * {@link #begin(String)} or {@link #beginEmpty(String)}. |
| * It stays open while calls to the <code>attribute()</code> |
| * methods are made. It is closed |
| * (the '>' is written) when any other method is invoked. |
| * |
| **/ |
| |
| private boolean _openTag = false; |
| |
| /** |
| * Indicates that the tag was opened with |
| * {@link #beginEmpty(String)}, which affects |
| * how the tag is closed (a slash is added to indicate the |
| * lack of a body). This is compatible with HTML, but reflects |
| * an XML/XHTML leaning. |
| * |
| * @since 2.2 |
| * |
| **/ |
| |
| private boolean _emptyTag = false; |
| |
| /** |
| * A Stack of Strings used to track the active tag elements. Elements are active |
| * until the corresponding close tag is written. The {@link #push(String)} method |
| * adds elements to the stack, {@link #pop()} removes them. |
| * |
| **/ |
| |
| private Stack _activeElementStack; |
| |
| /** |
| * The depth of the open tag stack. |
| * @see #_activeElementStack |
| * |
| **/ |
| |
| private int _depth = 0; |
| |
| private char[] _buffer; |
| |
| private String[] _entities; |
| private boolean[] _safe; |
| |
| /** |
| * Implemented in concrete subclasses to provide an indication of which |
| * characters are 'safe' to insert directly into the response. The index |
| * into the array is the character, if the value at the index is false (or the |
| * index out of range), then the character is escaped. |
| * |
| **/ |
| |
| private String _contentType; |
| |
| /** |
| * Indicates whether {@link #close()} should close the |
| * underlying {@link PrintWriter}. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| private boolean _propagateClose = true; |
| |
| public String getContentType() |
| { |
| return _contentType; |
| } |
| |
| abstract public IMarkupWriter getNestedWriter(); |
| |
| /** |
| * General constructor used by subclasses. |
| * |
| * @param safe an array of flags indicating which characters |
| * can be passed directly through with out filtering. Characters marked |
| * unsafe, or outside the range defined by safe, are converted to entities. |
| * @param entities a set of prefered entities, unsafe characters with |
| * a defined entity use the entity, other characters are converted |
| * to numeric entities. |
| * @param contentType the MIME type of the content produced by the writer. |
| * @param encoding the encoding of content produced by the writer. |
| * @param stream stream to which content will be written. |
| * |
| **/ |
| |
| protected AbstractMarkupWriter( |
| boolean safe[], |
| String[] entities, |
| String contentType, |
| String encoding, |
| OutputStream stream) |
| { |
| if (entities == null || safe == null || contentType == null || encoding == null) |
| throw new IllegalArgumentException( |
| Tapestry.getMessage("AbstractMarkupWriter.missing-constructor-parameters")); |
| |
| _entities = entities; |
| _safe = safe; |
| |
| _contentType = generateFullContentType(contentType, encoding); |
| |
| setOutputStream(stream, encoding); |
| } |
| |
| /** |
| * General constructor used by subclasses. |
| * This constructor is left for backward compatibility. It is preferred that |
| * it is not used since it does not specify an encoding for conversion. |
| * |
| * @param safe an array of flags indicating which characters |
| * can be passed directly through with out filtering. Characters marked |
| * unsafe, or outside the range defined by safe, are converted to entities. |
| * @param entities a set of prefered entities, unsafe characters with |
| * a defined entity use the entity, other characters are converted |
| * to numeric entities. |
| * @param contentType the type of content produced by the |
| * writer. |
| * @param stream stream to which content will be written. |
| **/ |
| |
| protected AbstractMarkupWriter( |
| boolean safe[], |
| String[] entities, |
| String contentType, |
| OutputStream stream) |
| { |
| this(safe, entities, contentType); |
| |
| ContentType contentTypeObject = new ContentType(contentType); |
| String encoding = contentTypeObject.getParameter("charset"); |
| |
| setOutputStream(stream, encoding); |
| } |
| |
| /** |
| * Creates new markup writer around the underlying {@link PrintWriter}. |
| * |
| * <p>This is primarily used by {@link org.apache.tapestry.engine.TagSupportService}, |
| * which is inlcuding content, and therefore this method will not |
| * close the writer when the markup writer is closed. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| protected AbstractMarkupWriter( |
| boolean safe[], |
| String[] entities, |
| String contentType, |
| PrintWriter writer) |
| { |
| this(safe, entities, contentType); |
| |
| // When the markup writer is closed, the underlying writer |
| // is NOT closed. |
| |
| _propagateClose = false; |
| _writer = writer; |
| } |
| |
| /** |
| * Special constructor used for nested response writers. |
| * The subclass is responsible for creating the writer. |
| * |
| **/ |
| |
| protected AbstractMarkupWriter(boolean safe[], String[] entities, String contentType) |
| { |
| if (entities == null || safe == null || contentType == null) |
| throw new IllegalArgumentException( |
| Tapestry.getMessage("AbstractMarkupWriter.missing-constructor-parameters")); |
| |
| _entities = entities; |
| _safe = safe; |
| _contentType = generateFullContentType(contentType, DEFAULT_ENCODING); |
| } |
| |
| /** |
| * Ensures that the content type has a charset (encoding) parameter. |
| * |
| * @param contentType The content type, e.g. text/html. It may contain a charset parameter. |
| * @param encoding The value of the charset parameter of the content type if it is not already present. |
| * @return The content type containing a charset parameter, e.g. text/html;charset=utf-8 |
| */ |
| private String generateFullContentType(String contentType, String encoding) |
| { |
| ContentType contentTypeObject = new ContentType(contentType); |
| if (contentTypeObject.getParameter("charset") == null) |
| contentTypeObject.setParameter("charset", encoding); |
| return contentTypeObject.unparse(); |
| } |
| |
| protected void setWriter(PrintWriter writer) |
| { |
| _writer = writer; |
| } |
| |
| protected void setOutputStream(OutputStream stream, String encoding) |
| { |
| try |
| { |
| OutputStreamWriter owriter; |
| if (encoding != null) |
| owriter = new OutputStreamWriter(stream, encoding); |
| else |
| owriter = new OutputStreamWriter(stream); |
| Writer bwriter = new BufferedWriter(owriter); |
| |
| _writer = new PrintWriter(bwriter); |
| } |
| catch (UnsupportedEncodingException e) |
| { |
| throw new IllegalArgumentException( |
| Tapestry.format("illegal-encoding", encoding)); |
| } |
| } |
| |
| /** |
| * Writes an integer attribute into the currently open tag. |
| * |
| * <p>TBD: Validate that name is legal. |
| * |
| * @throws IllegalStateException if there is no open tag. |
| * |
| **/ |
| |
| public void attribute(String name, int value) |
| { |
| checkTagOpen(); |
| |
| _writer.print(' '); |
| _writer.print(name); |
| _writer.print("=\""); |
| _writer.print(value); |
| _writer.print('"'); |
| } |
| |
| /** |
| * Writes a boolean attribute into the currently open tag. |
| * |
| * <p>TBD: Validate that name is legal. |
| * |
| * @throws IllegalStateException if there is no open tag. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| public void attribute(String name, boolean value) |
| { |
| checkTagOpen(); |
| |
| _writer.print(' '); |
| _writer.print(name); |
| _writer.print("=\""); |
| _writer.print(value); |
| _writer.print('"'); |
| } |
| |
| /** |
| * Writes an attribute into the most recently opened tag. This must be called after |
| * {@link #begin(String)} |
| * and before any other kind of writing (which closes the tag). |
| * |
| * <p>The value may be null. A null value will be rendered as an empty string. |
| * |
| * <p>Troublesome characters in the value are converted to thier GTML entities, much |
| * like a <code>print()</code> method, with the following exceptions: |
| * <ul> |
| * <li>The double quote (") is converted to &quot; |
| * <li>The ampersand (&) is passed through unchanged |
| * </ul> |
| * |
| * @throws IllegalStateException if there is no open tag. |
| * @param name The name of the attribute to write (no validation |
| * is done on the name). |
| * @param value The value to write. If null, the attribute |
| * name is written as the value. Otherwise, the |
| * value is written, |
| **/ |
| |
| public void attribute(String name, String value) |
| { |
| checkTagOpen(); |
| |
| _writer.print(' '); |
| |
| // Could use a check here that name contains only valid characters |
| |
| _writer.print(name); |
| _writer.print("=\""); |
| |
| if (value != null) |
| { |
| int length = value.length(); |
| |
| if (_buffer == null || _buffer.length < length) |
| _buffer = new char[length]; |
| |
| value.getChars(0, length, _buffer, 0); |
| |
| safePrint(_buffer, 0, length, true); |
| } |
| |
| _writer.print('"'); |
| |
| } |
| |
| /** |
| * Similar to {@link #attribute(String, String)} but no escaping of invalid elements |
| * is done for the value. |
| * |
| * @throws IllegalStateException if there is no open tag. |
| * |
| * @since 3.0 |
| * |
| **/ |
| public void attributeRaw(String name, String value) |
| { |
| if (value == null) |
| { |
| attribute(name, value); |
| return; |
| } |
| |
| checkTagOpen(); |
| |
| _writer.print(' '); |
| |
| _writer.print(name); |
| |
| _writer.print("=\""); |
| _writer.print(value); |
| _writer.print('"'); |
| } |
| |
| /** |
| * Closes any existing tag then starts a new element. The new element is pushed |
| * onto the active element stack. |
| **/ |
| |
| public void begin(String name) |
| { |
| if (_openTag) |
| closeTag(); |
| |
| push(name); |
| |
| _writer.print('<'); |
| _writer.print(name); |
| |
| _openTag = true; |
| _emptyTag = false; |
| } |
| |
| /** |
| * Starts an element that will not later be matched with an <code>end()</code> |
| * call. This is useful for elements such as <hr;> or <br> that |
| * do not need closing tags. |
| * |
| **/ |
| |
| public void beginEmpty(String name) |
| { |
| if (_openTag) |
| closeTag(); |
| |
| _writer.print('<'); |
| _writer.print(name); |
| |
| _openTag = true; |
| _emptyTag = true; |
| } |
| |
| /** |
| * Invokes <code>checkError()</code> on the |
| * <code>PrintWriter</code> used to format output. |
| **/ |
| |
| public boolean checkError() |
| { |
| return _writer.checkError(); |
| } |
| |
| private void checkTagOpen() |
| { |
| if (!_openTag) |
| throw new IllegalStateException( |
| Tapestry.getMessage("AbstractMarkupWriter.tag-not-open")); |
| } |
| |
| /** |
| * Closes this <code>IMarkupWriter</code>. Any active elements are closed. The |
| * {@link PrintWriter} is then sent {@link PrintWriter#close()}. |
| * |
| **/ |
| |
| public void close() |
| { |
| if (_openTag) |
| closeTag(); |
| |
| // Close any active elements. |
| |
| while (_depth > 0) |
| { |
| _writer.print("</"); |
| _writer.print(pop()); |
| _writer.print('>'); |
| } |
| |
| if (_propagateClose) |
| _writer.close(); |
| |
| _writer = null; |
| _activeElementStack = null; |
| _buffer = null; |
| } |
| |
| /** |
| * Closes the most recently opened element by writing the '>' that ends |
| * it. May write a slash before the '>' if the tag |
| * was opened by {@link #beginEmpty(String)}. |
| * |
| * <p>Once this is invoked, the <code>attribute()</code> methods |
| * may not be used until a new element is opened with {@link #begin(String)} or |
| * or {@link #beginEmpty(String)}. |
| **/ |
| |
| public void closeTag() |
| { |
| if (_emptyTag) |
| _writer.print('/'); |
| |
| _writer.print('>'); |
| |
| _openTag = false; |
| _emptyTag = false; |
| } |
| |
| /** |
| * Writes an GTML comment. Any open tag is first closed. |
| * The method takes care of |
| * providing the <code><!--</code> and <code>--></code>, |
| * including a blank line after the close of the comment. |
| * |
| * <p>Most characters are valid inside an GTML comment, so no check |
| * of the contents is made (much like {@link #printRaw(String)}. |
| * |
| **/ |
| |
| public void comment(String value) |
| { |
| if (_openTag) |
| closeTag(); |
| |
| _writer.print("<!-- "); |
| _writer.print(value); |
| _writer.println(" -->"); |
| } |
| |
| /** |
| * Ends the element most recently started by {@link #begin(String)}. |
| * The name of the tag |
| * is popped off of the active element stack and used to form an GTML close tag. |
| * |
| * <p>TBD: Error checking for the open element stack empty. |
| **/ |
| |
| public void end() |
| { |
| if (_openTag) |
| closeTag(); |
| |
| _writer.print("</"); |
| _writer.print(pop()); |
| _writer.print('>'); |
| } |
| |
| /** |
| * Ends the most recently started element with the given name. This will |
| * also end any other intermediate elements. This is very useful for easily |
| * ending a table or even an entire page. |
| * |
| * <p>TBD: Error check if the name matches nothing on the open tag stack. |
| **/ |
| |
| public void end(String name) |
| { |
| if (_openTag) |
| closeTag(); |
| |
| while (true) |
| { |
| String tagName = pop(); |
| |
| _writer.print("</"); |
| _writer.print(tagName); |
| _writer.print('>'); |
| |
| if (tagName.equals(name)) |
| break; |
| } |
| } |
| |
| /** |
| * Forwards <code>flush()</code> to this <code>AbstractMarkupWriter</code>'s |
| * <code>PrintWriter</code>. |
| * |
| **/ |
| |
| public void flush() |
| { |
| _writer.flush(); |
| } |
| |
| /** |
| * Removes the top element from the active element stack and returns it. |
| * |
| **/ |
| |
| protected final String pop() |
| { |
| String result = (String) _activeElementStack.pop(); |
| _depth--; |
| |
| return result; |
| } |
| |
| /** |
| * |
| * The primary <code>print()</code> method, used by most other methods. |
| * |
| * <p>Prints the character array, first closing any open tag. Problematic characters |
| * ('<', '>' and '&') are converted to their |
| * GTML entities. |
| * |
| * <p>All 'unsafe' characters are properly converted to either a named |
| * or numeric GTML entity. This can be somewhat expensive, so use |
| * {@link #printRaw(char[], int, int)} if the data to print is guarenteed |
| * to be safe. |
| * |
| * <p>Does <em>nothing</em> if <code>data</code> is null. |
| * |
| * <p>Closes any open tag. |
| * |
| **/ |
| |
| public void print(char[] data, int offset, int length) |
| { |
| if (data == null) |
| return; |
| |
| if (_openTag) |
| closeTag(); |
| |
| safePrint(data, offset, length, false); |
| } |
| |
| /** |
| * Prints a single character. If the character is not a 'safe' character, |
| * such as '<', then it's GTML entity (named or numeric) is printed instead. |
| * |
| * <p>Closes any open tag. |
| * |
| **/ |
| |
| public void print(char value) |
| { |
| if (_openTag) |
| closeTag(); |
| |
| if (value < _safe.length && _safe[value]) |
| { |
| _writer.print(value); |
| return; |
| } |
| |
| String entity = null; |
| |
| if (value < _entities.length) |
| entity = _entities[value]; |
| |
| if (entity != null) |
| { |
| _writer.print(entity); |
| return; |
| } |
| |
| // Not a well-known entity. Print it's numeric equivalent. Note: this omits |
| // the leading '0', but most browsers (IE 5.0) don't seem to mind. Is this a bug? |
| |
| _writer.print("&#" + (int) value + ";"); |
| } |
| |
| /** |
| * Prints an integer. |
| * |
| * <p>Closes any open tag. |
| * |
| **/ |
| |
| public void print(int value) |
| { |
| if (_openTag) |
| closeTag(); |
| |
| _writer.print(value); |
| } |
| |
| /** |
| * Invokes {@link #print(char[], int, int)} to print the string. Use |
| * {@link #printRaw(String)} if the character data is known to be safe. |
| * |
| * <p>Does <em>nothing</em> if <code>value</code> is null. |
| * |
| * <p>Closes any open tag. |
| * |
| * @see #print(char[], int, int) |
| * |
| **/ |
| |
| public void print(String value) |
| { |
| if (value == null) |
| return; |
| |
| int length = value.length(); |
| |
| if (_buffer == null || _buffer.length < length) |
| _buffer = new char[length]; |
| |
| value.getChars(0, length, _buffer, 0); |
| |
| print(_buffer, 0, length); |
| } |
| |
| /** |
| * Closes the open tag (if any), then prints a line seperator to the output stream. |
| * |
| **/ |
| |
| public void println() |
| { |
| if (_openTag) |
| closeTag(); |
| |
| _writer.println(); |
| } |
| |
| /** |
| * Prints and portion of an output buffer to the stream. |
| * No escaping of invalid GTML elements is done, which |
| * makes this more effecient than <code>print()</code>. |
| * Does <em>nothing</em> if <code>buffer</code> |
| * is null. |
| * |
| * <p>Closes any open tag. |
| * |
| **/ |
| |
| public void printRaw(char[] buffer, int offset, int length) |
| { |
| if (buffer == null) |
| return; |
| |
| if (_openTag) |
| closeTag(); |
| |
| _writer.write(buffer, offset, length); |
| } |
| |
| /** |
| * Prints output to the stream. No escaping of invalid GTML elements is done, which |
| * makes this more effecient than <code>print()</code>. Does <em>nothing</em> |
| * if <code>value</code> |
| * is null. |
| * |
| * <p>Closes any open tag. |
| * |
| **/ |
| |
| public void printRaw(String value) |
| { |
| if (value == null) |
| return; |
| |
| if (_openTag) |
| closeTag(); |
| |
| _writer.print(value); |
| } |
| |
| /** |
| * Adds an element to the active element stack. |
| * |
| **/ |
| |
| protected final void push(String name) |
| { |
| if (_activeElementStack == null) |
| _activeElementStack = new Stack(); |
| |
| _activeElementStack.push(name); |
| |
| _depth++; |
| } |
| |
| /** |
| * Internal support for safe printing. Ensures that all characters emitted |
| * are safe: either valid GTML characters or GTML entities (named or numeric). |
| **/ |
| |
| private void safePrint(char[] data, int offset, int length, boolean isAttribute) |
| { |
| int safelength = 0; |
| int start = offset; |
| |
| for (int i = 0; i < length; i++) |
| { |
| char ch = data[offset + i]; |
| |
| // Ignore safe characters. In an attribute, quotes |
| // are not ok and are escaped. |
| |
| boolean isSafe = (ch < _safe.length && _safe[ch]); |
| |
| if (isAttribute && ch == '"') |
| isSafe = false; |
| |
| if (isSafe) |
| { |
| safelength++; |
| continue; |
| } |
| |
| // Write the safe stuff. |
| |
| if (safelength > 0) |
| _writer.write(data, start, safelength); |
| |
| String entity = null; |
| |
| // Look for a known entity. |
| |
| if (ch < _entities.length) |
| entity = _entities[ch]; |
| |
| // Failing that, emit a numeric entity. |
| |
| if (entity == null) |
| entity = "&#" + (int) ch + ";"; |
| |
| _writer.print(entity); |
| |
| start = offset + i + 1; |
| safelength = 0; |
| } |
| |
| if (safelength > 0) |
| _writer.write(data, start, safelength); |
| } |
| |
| } |