/******************************************************************************* * Copyright (c) 2002, 2008 Innoopract Informationssysteme GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Innoopract Informationssysteme GmbH - initial API and implementation ******************************************************************************/ package org.eclipse.rwt.internal.lifecycle; import java.io.IOException; import java.io.Writer; import java.util.*; import org.eclipse.rwt.internal.util.*; /** * <p>This class assists in writing markup to the response stream.</p> * <p><b>Note:</b> Dont't use any of the <code>write</code>-Methods to start * or end an <em>element</em>. Doing so will confuse 'outer' rendering code * and may lead to orphan closing angle brackets (>). To start and end * <em>elements</em> only use the semantic-aware methods * <code>startElement</code>, <code>endElement</code> and * <code>closeElementIfStarted</code>.</p> */ public class HtmlResponseWriter extends Writer { /** <p>used for rendering internally.</p> * * <p>Styles with exactly the same settings are collected while rendering * and a css class with these settings and prefixed CLASS_PREFIX is * rendered and assigned instead of rendering the same style settings * inline multiple times. This happens only if no class attribute * (see {@link SimpleComponent#getCssClass() SimpleComponent.getCssClass()}) * is set; else style settings are inlined into the HTML. Note that in the * latter case the rendering and style interprettion behaviour of the * browser may cause some or all of either the style or the class settings * to take no effect.</p> */ // TODO [w4t] moved from Style, move back when dependency is resolved public static final String CLASS_PREFIX = "w4tCss"; private List body = new ArrayList(); private String elementStarted; private boolean closed; private boolean avoidEscape; /** contains the names of the javascript libraries that are needed for * the content which was rendered into this HtmlResponseWriter. */ private List jsLibraries = new ArrayList(); /** * <p>Append a token to the token list of the body's token</p> * <p>This method is not inteded to be used by clients.</p> * */ public void append( final String token ) { body.add( token ); } /** * <p>Append a HtmlResponseWriter's body token list to the * token list of the body's token.</p> * <p>This method is not inteded to be used by clients.</p> */ public void append( final HtmlResponseWriter responseWriter ) { for( int i = 0; i < responseWriter.getBodySize(); i++ ) { body.add( responseWriter.getBodyToken( i ) ); } } /** * <p>Removes all of the tokens from the body list. The body list will be * empty after this call returns</p> * <p>This method is not inteded to be used by clients.</p> */ public void clearBody() { body.clear(); } /** * <p>Returns the number of tokens in the body list.</p> * <p>This method is not inteded to be used by clients.</p> */ public int getBodySize() { return body.size(); } /** * <p>Returns the token at the specified position in the body list.</p> * <p>This method is not inteded to be used by clients.</p> */ public String getBodyToken( final int index ) { return body.get( index ).toString(); } /** * <p>Returns an iterator to loop over all body tokens.</p> * <p>This method is not inteded to be used by clients.</p> */ public Iterator bodyTokens() { return body.iterator(); } /////////////////////////////////////////////////// // control methods for javascript library rendering /** <p>Returns the names of the JavaScript libraries that the components * which were rendered into this HtmlResponseWriter need.</p> */ public String[] getJSLibraries() { String[] result = new String[ jsLibraries.size() ]; jsLibraries.toArray( result ); return result; } /** * <p>Informs the HtmlResponseWriter that the given library is needed. This * will cause a <script>-tag referencing the library to be rendered at * the adequate place.</p> * <p>Prior to calling this methid, the given <code>libraryName</code> must * be registered with the {@link org.eclipse.rwt.resources.IResourceManager * IResourceManager} using one of these two register-methods: * {@link org.eclipse.rwt.resources.IResourceManager#register(String, String) * register(String, String)}, * {@link org.eclipse.rwt.resources.IResourceManager#register(String, String, * org.eclipse.rwt.resources.IResourceManager.RegisterOptions) * register(String, String, RegisterOptions)}</p> * <p>Calling this method for an already registered <code>libraryName</code> * has no effect.</p> * @param libraryName the name of the library, must not be <code>null</code>. */ public void useJSLibrary( final String libraryName ) { ParamCheck.notNull( libraryName, "libraryName" ); if( !jsLibraries.contains( libraryName ) ) { jsLibraries.add( libraryName ); } } /** * <p>Removes the given <code>libraryName</code> from the list of JavaScript * libraries.</p> * <p>An attempt to remove a not previously registered * (using <code>useJSLibrary(String)</code>) library will be ignored silently. * </p> * @param libraryName the name of the library to be removed */ public void removeJSLibraries( final String libraryName ) { jsLibraries.remove( libraryName ); } /** * <p>Returns the number of JavaScript libraries that were registered by * calls to <code>useJSLibrary(String)</code>.</p> */ public int getJSLibrariesCount() { return jsLibraries.size(); } ////////////////// // response writer public void close() throws IOException { checkIfWriterClosed(); closeElementIfStarted(); closed = true; } public void flush() throws IOException { checkIfWriterClosed(); closeElementIfStarted(); } public void write( final char[] cbuf, final int off, final int len ) throws IOException { checkIfWriterClosed(); closeElementIfStarted(); doWrite( cbuf, off, len ); } /** * <p>Writes the given <code>character</code> to the response stream.</p> * @param character the single char to be written * @throws IOException if an I/O error occurs */ public void write( final char character ) throws IOException { checkIfWriterClosed(); // TODO [rh] replace with doWrite(String) ? append( new String( new char[]{ character } ) ); } public void write( final int c ) throws IOException { checkIfWriterClosed(); // TODO [rh] replace with doWrite(String) ? append( new String( new char[] { ( char )c } ) ); } public void write( final String content ) throws IOException { checkIfWriterClosed(); closeElementIfStarted(); doWrite( content ); } public void write( final String str, final int off, final int len ) throws IOException { checkIfWriterClosed(); // TODO [rh] replace with doWrite(String) ? append( str.substring( off, off + len ) ); } /** * <p>Starts the element given by <code>name</code>. An eventually still * opened element will be closed first.</p> * @param name the elements name, must not be <code>null</code>. * @param component <em>currently not used, must be <code>null</code></em>. * @throws IOException if an I/O error occurs */ public void startElement( final String name, final Object component ) throws IOException { checkIfWriterClosed(); ParamCheck.notNull( name, "name" ); closeElementIfStarted(); // checking first with charAt for performance, since equalsIgnoreCase check // takes twice as long char firstChar = name.charAt( 0 ); if( ( firstChar == 's' ) || ( firstChar == 'S' ) ) { if( "script".equalsIgnoreCase( name ) || "style".equalsIgnoreCase( name ) ) { avoidEscape = true; } } doWrite( "<" ); doWrite( name ); elementStarted = name; } /** * <p>Writes an attribute to the currently started element. Characters * not allowed in (X)HTML will be encoded.</p> * <p>Example: * <pre> * writer.startElement("input",null); * writer.writeAttribute("hidden",null,null); * writer.endElement("input"); * // results in <input hidden="hidden" /> * </pre> * </p> * @param name the attribtues name, must not be <code>null</code>. * @param value the attributes value. If <code>null</code> it is considered * a 'minimized' attribute. * @param property <em>currently not used, must be <code>null</code></em>. * @throws IOException if an I/O error occurs * @throws IllegalStateException when there is no started element */ public void writeAttribute( final String name, final Object value, final String property ) throws IOException { checkIfWriterClosed(); ParamCheck.notNull( name, "name" ); if( elementStarted == null ) { String msg = "There is no started element to add an attribute."; throw new IllegalStateException( msg ); } if( value == null ) { doWrite( " " ); doWrite( name ); doWrite( "=\"" ); doWrite( name ); doWrite( "\"" ); } else { doWrite( " " ); doWrite( name ); doWrite( "=\"" ); doWrite( HtmlResponseWriterUtil.encode( value.toString() ) ); doWrite( "\"" ); } } /** * <p>Writes the closing tag for element <code>name</code> to the response * stream.</p> * @param name the elements name, must not be <code>null</code>. * @throws IOException if an I/O error occurs */ public void endElement( final String name ) throws IOException { checkIfWriterClosed(); ParamCheck.notNull( name, "name" ); closeElementIfStarted(); avoidEscape = false; if( !HtmlResponseWriterUtil.isEmptyTag( name ) ) { doWrite( "</" ); doWrite( name ); doWrite( ">" ); } } /** * <p>Ends the document. An eventually opened element is closed.</p> * @throws IOException if an I/O error occurs */ public void endDocument() throws IOException { checkIfWriterClosed(); closeElementIfStarted(); } /** * <p>Writes the <em>toString</em> of the given <code>text</code> to the * response stream, characters not allowed in HTML will be encoded. Use * this method only to write 'inside' an element.</p> * <p>Example: * <pre> * writer.startElement("p",null); * <strong>writer.writeText("Hello World");</strong> * writer.endElement("p"); * </pre></p> * @param text the text to write, must not be <code>null</code>. * @param property <em>currently not used, must be <code>null</code></em> * @throws IOException if an I/O error occurs */ // TODO [rh] surround content of script tags with [CDATA[ section public void writeText( final Object text, final String property ) throws IOException { checkIfWriterClosed(); ParamCheck.notNull( text, "text" ); closeElementIfStarted(); if( avoidEscape ) { doWrite( text.toString() ); } else { doWrite( HtmlResponseWriterUtil.encode( text.toString() ) ); } } /** * <p>Writes a portion of an array of characters.</p> * @param text array of characters, must not be <code>null</code> * @param off offset from which to start writing characters * @param len number of characters to write * @throws IOException if an I/O error occurs */ public void writeText( final char[] text, final int off, final int len ) throws IOException { checkIfWriterClosed(); ParamCheck.notNull( text, "text" ); closeElementIfStarted(); if( avoidEscape ) { doWrite( text, off, len ); } else { char[] content = new char[ len ]; System.arraycopy( text, off, content, 0, len ); StringBuffer buffer = new StringBuffer(); buffer.append( HtmlResponseWriterUtil.encode( new String( content ) ) ); char[] encoded = buffer.toString().toCharArray(); write( encoded ); } } /** * <p>Writes the given <code>comment</code>, surrounded by opening and * closing comment tags (<!-- -->), to the response stream.</p> * @param comment the comment to be written, must not be <code>null</code>. * @throws IOException if an I/O error occurs */ // TODO [rh] We could check whether 'elementStarted' is null, since comments // are not allowed inside element tags in XHTML 'mode' public void writeComment( final Object comment ) throws IOException { checkIfWriterClosed(); ParamCheck.notNull( comment, "comment" ); closeElementIfStarted(); doWrite( "<!-- " ); doWrite( HtmlResponseWriterUtil.encode( comment.toString() ) ); doWrite( " -->" ); } /** * <p>Writes a non-breaking space (&nbsp;) to the response stream.</p> * @throws IOException if an I/O error occurs */ public void writeNBSP() throws IOException { checkIfWriterClosed(); closeElementIfStarted(); doWrite( " " ); } /** * <p>Starts the document</p> * @throws IOException if an I/O error occurs */ public void startDocument() throws IOException { checkIfWriterClosed(); } /** * <p>Writes appropriate markup to close an eventually started element (see * {@link #startElement(String, Object) startElement(String,Object)}). Does * nothing if no element was started.</p> * <p>Usually it is not necessary to call this method explicitly, since * the semantic-aware methods like <code>writeText</code> and * <code>closeElement</code> take care about not yet closed elements.</p> * <p>Example: * <pre> * writer.startElement("div",null); * writer.writeAttribute("id","x",null); * writer.closeElementIfStarted(); * // results in <div id="x" /> * </pre></p> * @throws IOException if an I/O error occurs */ public void closeElementIfStarted() { if( elementStarted != null ) { if( HtmlResponseWriterUtil.isEmptyTag( elementStarted ) ) { doWrite( " />" ); } else { doWrite( ">" ); } elementStarted = null; } } protected void doWrite( final String content ) { append( content ); } private void doWrite( final char[] cbuf, final int off, final int len ) { // TODO [rh] replace with doWrite(String) ? append( String.valueOf( cbuf, off, len ) ); } // helping methods ////////////////// private void checkIfWriterClosed() { if( closed ) { String msg = "Operation is not allowed since the writer was closed."; throw new IllegalStateException( msg ); } } }