Re: "BUG found !?" or "a question..." / was: Re: jigsaw 2.0.2 bug in servlet API !?

Ok, here is a patch for servlets, this should fix your bug.
I modified JigsawHttpServletResponse, now only replies of
included request (SSI, Dispatcher, ...) use ByteArrayOutputStream.

I modified JigsawServletOutputStream, now Headers are sent when the
servlet start to write on the stream, not when the servlet call getWriter().

Modified files are attached to this mail.

Regards, Benoit

Roland Mainz wrote:

> Hi !
>
> ----
>
> Benoit Mahe wrote:
>
> > flushStream is called only by ServletWrapper after the service method call,
> > so the get method is done at this time.
> >
> > But, may be there could be a problem in some case, please add the line:
> > out.flush();
> > just before the reply.setContentLength(out.size());
> >
> > Could you try this and tell me if the bug is still there? If not could you
> > check (by adding some trace) if out.size() is equals to the Content Length
> > you specified in your servlet?
>
> Tested, and the size printed in the debugging output looks ok.
>
> Example:
>
> -- snip --
> JigsawHttpServletResponse: init
> DownloadServlet: access from null@spiderman.informatik.med.uni-giessen.de
> (141.50.227.118)
> DownloadServlet: file requested: 'saxjava-1.0.zip'
> DownloadServlet: sending '/home/gisburn/download/xml-stuff/saxjava-1.0.zip'
> (application/octet-stream)
> DownloadServlet: transfer of /home/gisburn/download/xml-stuff/saxjava-1.0.zip
> (120250/120250) done.
> JigsawHttpServletResponse: using serlvet given length: 120250 (sending 120250 bytes).
> JigsawHttpServletResponse: sending 120250 bytes.
> JigsawHttpServletResponse: state != INCLUDED
> -- snip --
>
> I've attachmned "JigsawHttpServletResponse.java" in my "Need help..." message to you;
> this version
> I make the test with only adds out.flush() (but it seems that there is no differernce
> :-(     ), IMHO
> the problem sits somewhere much deeper, or my servlet code is simply: WRONG... (or
> not...) =:-)
>
> ----
>
> Bye,
> Roland
>
> --
>   __ .  . __
>  (o.\ \/ /.o)  Roland Mainz                               C programmer
>   \__\/\/__/   Roland.Mainz@informatik.med.uni-giessen.de MPEG specialist
>   /O /==\ O\   gisburn@w-specht.rhein-ruhr.de             Sun&&Amiga programmer
>  (;O/ \/ \O;)  TEL +49 (0) 2426901568  FAX +49 (0) 2426901569

--
- Benoît Mahé -------------------------------------------------------
                      World Wide Web Consortium (W3C)
                    Architecture domain - Jigsaw Team

  http://www.w3.org/People/Mahe - bmahe@w3.org - +33.4.92.38.79.89
---------------------------------------------------------------------
// JigsawHttpServletReponse.java
// $Id: JigsawHttpServletResponse.java,v 1.30 1999/04/07 08:58:01 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.net.*;

import org.w3c.www.mime.*;
import org.w3c.www.http.*;
import org.w3c.jigsaw.http.*;

/**
 * @author Alexandre Rafalovitch <alex@access.com.au>
 * @author Anselm Baird-Smith <abaird@w3.org>
 * @author Benoît Mahé (bmahe@w3.org)
 * @author Roland Mainz (Roland.Mainz@informatik.med.uni-giessen.de)
 */

public class JigsawHttpServletResponse implements HttpServletResponse {

    private final static int STATE_INITIAL = 0;
    private final static int STATE_HEADERS_DONE = 1;
    private final static int STATE_ALL_DONE = 2;

    private final static int STREAM_STATE_INITIAL = 0;
    private final static int STREAM_WRITER_USED = 1;
    private final static int OUTPUT_STREAM_USED = 2;

    private int stream_state = STREAM_STATE_INITIAL;

    private JigsawServletOutputStream output = null;
    private PrintWriter               writer = null;
 
    private MimeTypeFormatException setContentTypeException = null;

    // servlet has set a fixed content length or not, 
    // and cut (see flushStream) any data which are too much here...
    private final static int CALC_CONTENT_LENGTH = -1; 
    private              int fixedContentLength  = CALC_CONTENT_LENGTH;

    /**
     * Our temp stream.
     */
    protected ByteArrayOutputStream out = null;

    protected JigsawHttpServletRequest jrequest = null;

    protected void setServletRequest(JigsawHttpServletRequest jrequest) {
	this.jrequest = jrequest;
    }

    public static final 
	String INCLUDED = "org.w3c.jigsaw.servlet.included";

    int   state = STATE_INITIAL;
    Reply reply = null;
    Request request = null;
    
    /**
     * Sets the content length for this response. 
     * @param len - the content length 
     */
    public void setContentLength(int i) {
	fixedContentLength = i;
	reply.setContentLength(i);
    }

    /**
     * Sets the content type for this response. This type may later be 
     * implicitly modified by addition of properties such as the MIME
     * charset=<value> if the service finds it necessary, and the appropriate
     * media type property has not been set.
     * <p>This response property may only be assigned one time. If a writer 
     * is to be used to write a text response, this method must be
     * called before the method getWriter. If an output stream will be used 
     * to write a response, this method must be called before the
     * output stream is used to write response data. 
     * @param spec - the content's MIME type 
     * @see JigsawHttpServletResponse#getOutputStream
     * @see JigsawHttpServletResponse#getWriter
     */    
    public void setContentType(String spec) {
	try {
	    MimeType type= new MimeType(spec);
	    reply.setContentType(type);
	    setContentTypeException = null;
	} catch(MimeTypeFormatException ex) {
	    //store exception
	    setContentTypeException = ex;
	}
    }

    protected boolean isStreamObtained() {
	return (stream_state != STREAM_STATE_INITIAL);
    }

    protected Reply getReply() {
	return reply;
    }

    /**
     * Returns an output stream for writing binary response data.
     * @return A ServletOutputStream
     * @exception IOException if an I/O exception has occurred 
     * @exception IllegalStateException if getWriter has been called on this 
     * same request. 
     * @see JigsawHttpServletResponse#getWriter
     */    
    public synchronized ServletOutputStream getOutputStream()
	throws IOException
    {
	if (stream_state == STREAM_WRITER_USED)
	    throw new IllegalStateException("Writer used");
	stream_state = OUTPUT_STREAM_USED;
	return getJigsawOutputStream();
    }

    protected ServletOutputStream getJigsawOutputStream()
	throws IOException
    {
	if ( output != null )
	    return output;

	// any exception during setContentType ?    
	if( setContentTypeException != null ) {
	    // "wrap" the exception from setContentType in an IOException
	    throw new IOException("Illegal Contenttype: "+
				  setContentTypeException.toString());
	}

	if( request.hasState(INCLUDED) ) {
	    out = new ByteArrayOutputStream();
	    output = new JigsawServletOutputStream(this, 
						   new DataOutputStream(out));
	} else {
	    output = new JigsawServletOutputStream(this, reply);
	}
	return output;
    }

    /**
     * Sets the status code and message for this response. If the field had
     * already been set, the new value overwrites the previous one. The message
     * is sent as the body of an HTML page, which is returned to the user to
     * describe the problem. The page is sent with a default HTML header; the
     * message is enclosed in simple body tags (<body></body>).
     * @param i - the status code 
     * @param reason - the status message
     * @deprecated since jsdk2.1
     */
    public void setStatus(int i, String reason) {
	reply.setStatus(i);
	reply.setReason(reason);
    }
    
    /**
     * Sets the status code for this response. This method is used to set the
     * return status code when there is no error (for example, for the status
     * codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, the 
     * sendError method should be used instead.
     * @param i - the status code 
     * @see JigsawHttpServletResponse#sendError
     */
    public void setStatus(int i) {
	setStatus(i, reply.getStandardReason(i));
    }
    
    /**
     * Adds a field to the response header with the given name and value. If
     * the field had already been set, the new value overwrites the previous
     * one. The containsHeader method can be used to test for the presence of a
     * header before setting its value.
     * @param name - the name of the header field 
     * @param value - the header field's value 
     * @see JigsawHttpServletResponse#containsHeader
     */
    public void setHeader(String name, String value) {
	reply.setValue(name, value);
    }
    
    /**
     * Adds a field to the response header with the given name and integer
     * value. If the field had already been set, the new value overwrites the
     * previous one. The containsHeader method can be used to test for the
     * presence of a header before setting its value.
     * @param name - the name of the header field 
     * @param value - the header field's integer value 
     * @see JigsawHttpServletResponse#containsHeader
     */
    public void setIntHeader(String name, int value) {
	setHeader(name, String.valueOf(value));
    }
    
    /**
     * Adds a field to the response header with the given name and date-valued
     * field. The date is specified in terms of milliseconds since the epoch. 
     * If the date field had already been set, the new value overwrites the
     * previous one. The containsHeader method can be used to test for the
     * presence of a header before setting its value.
     * @param name - the name of the header field 
     * @param value - the header field's date value 
     * @see JigsawHttpServletResponse#containsHeader 
     */
    public void setDateHeader(String name, long date) {
	setHeader(name, String.valueOf(date));
    }
    
    public void unsetHeader(String name) {
	setHeader(name, null);
    }
    
    /**
     * Sends an error response to the client using the specified status code
     * and descriptive message. If setStatus has previously been called, it is
     * reset to the error status code. The message is sent as the body of an
     * HTML page, which is returned to the user to describe the problem. The
     * page is sent with a default HTML header; the message is enclosed in
     * simple body tags (<body></body>).
     * @param sc - the status code 
     * @param msg - the detail message 
     * @exception IOException If an I/O error has occurred.
     */
    public void sendError(int i, String msg) 
	throws IOException
    {
	setStatus(i);
	reply.setContent(msg);
	state = STATE_ALL_DONE;
    }

    /**
     * Sends an error response to the client using the specified status 
     * code and a default message. 
     * @param sc - the status code 
     * @exception IOException If an I/O error has occurred.
     */
    public void sendError(int i)
        throws IOException
    {
	setStatus(i);
	reply.setContent(reply.getStandardReason(i));
	state = STATE_ALL_DONE;
    }
    
    /**
     * Sends a temporary redirect response to the client using the specified
     *  redirect location URL. The URL must be absolute (for example, 
     * https://hostname/path/file.html). Relative URLs are not permitted here. 
     * @param url - the redirect location URL 
     * @exception IOException If an I/O error has occurred. 
     */    
    public void sendRedirect(String url)
        throws IOException
    {
	URL loc = null;
	try {
	    loc = new URL(request.getURL(), url);
	    setStatus(SC_MOVED_TEMPORARILY);
	    reply.setLocation(loc);
	    state = STATE_ALL_DONE;
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }

    /**
     * Checks whether the response message header has a field with the
     * specified name. 
     * @param name - the header field name 
     * @return true if the response message header has a field with the 
     * specified name; false otherwise
     */
    public boolean containsHeader(String header) {
	return reply.hasHeader(header);
    }

    /**
     * Adds the specified cookie to the response. It can be called multiple 
     * times to set more than one cookie. 
     * @param cookie - the Cookie to return to the client 
     */
    public void addCookie(Cookie cookie) {
	HttpSetCookieList clist = reply.getSetCookie();
	if (clist == null) {
	    HttpSetCookie cookies [] = new HttpSetCookie[1];
	    cookies[0] = convertCookie(cookie);
	    clist = new HttpSetCookieList(cookies);
	} else {
	    clist.addSetCookie(convertCookie(cookie));
	}
	reply.setSetCookie(clist);
    }

    public HttpSetCookie convertCookie(Cookie cookie) {
	HttpSetCookie scookie = new HttpSetCookie(true, 
						  cookie.getName(),
						  cookie.getValue());
	scookie.setComment(cookie.getComment());
	scookie.setDomain(cookie.getDomain());
	scookie.setMaxAge(cookie.getMaxAge());
	scookie.setPath(cookie.getPath());
	scookie.setSecurity(cookie.getSecure());
	scookie.setVersion(cookie.getVersion());
	return scookie;
    }

    /**
     * Encodes the specified URL for use in the sendRedirect method or, if 
     * encoding is not needed, returns the URL unchanged. The implementation 
     * of this method should include the logic to determine whether the 
     * session ID needs to be encoded in the URL.
     * Because the rules for making this determination differ from those used
     * to decide whether to encode a normal link, this method is seperate from
     * the encodeUrl method.
     * <p>All URLs sent to the HttpServletResponse.sendRedirect method should
     * be run through this method. Otherwise, URL rewriting canont be used 
     * with browsers which do not support cookies. 
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     * @deprecated since jsdk2.1
     * @see JigsawHttpServletResponse#sendRedirect
     * @see JigsawHttpServletResponse#encodeUrl
     */
    public String encodeRedirectUrl(String url) {
	try {
	    URL redirect = new URL(url);
	    URL requested = new URL(jrequest.getRequestURI());
	    if ( redirect.getHost().equals(requested.getHost()) &&
		 redirect.getPort() == requested.getPort())
		return encodeUrl(url);
	} catch (MalformedURLException ex) {
	    //error so return url.
	    return url;
	}
	return url;
    }

        /**
     * Encodes the specified URL for use in the sendRedirect method or, if 
     * encoding is not needed, returns the URL unchanged. The implementation 
     * of this method should include the logic to determine whether the 
     * session ID needs to be encoded in the URL.
     * Because the rules for making this determination differ from those used
     * to decide whether to encode a normal link, this method is seperate from
     * the encodeUrl method.
     * <p>All URLs sent to the HttpServletResponse.sendRedirect method should
     * be run through this method. Otherwise, URL rewriting canont be used 
     * with browsers which do not support cookies. 
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     * @see JigsawHttpServletResponse#sendRedirect
     * @see JigsawHttpServletResponse#encodeUrl
     */
    public String encodeRedirectURL(String url) {
	return encodeRedirectUrl(url);
    }

    /**
     * Encodes the specified URL by including the session ID in it, or, if 
     * encoding is not needed, returns the URL unchanged. The implementation of
     * this method should include the logic to determine whether the session ID
     * needs to be encoded in the URL. For example, if the browser supports
     * cookies, or session tracking is turned off, URL encoding is unnecessary.
     * <p>All URLs emitted by a Servlet should be run through this method. 
     * Otherwise, URL rewriting cannot be used with browsers which do not 
     * support cookies.
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     * @deprecated since jsdk2.1
     */
    public String encodeUrl(String url) {
	if (! jrequest.isRequestedSessionIdFromCookie()) {
	    url = url + ((url.indexOf("?") != -1) ? "&" : "?")+
		jrequest.getCookieName()+"="+
		jrequest.getSession(true).getId();
	}
	return url;
    }

    /**
     * Encodes the specified URL by including the session ID in it, or, if 
     * encoding is not needed, returns the URL unchanged. The implementation of
     * this method should include the logic to determine whether the session ID
     * needs to be encoded in the URL. For example, if the browser supports
     * cookies, or session tracking is turned off, URL encoding is unnecessary.
     * <p>All URLs emitted by a Servlet should be run through this method. 
     * Otherwise, URL rewriting cannot be used with browsers which do not 
     * support cookies.
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     */
    public String encodeURL(String url) {
	return encodeUrl(url);
    }

    /**
     * Return the Charset parameter of content type
     * @return A String instance
     */
    public String getCharacterEncoding() {
	org.w3c.www.mime.MimeType type = reply.getContentType();
	if ((type != null) && (type.hasParameter("charset"))) {
	    return type.getParameterValue("charset");
	}
	return System.getProperty("file.encoding");
    }

    /**
     * Returns a print writer for writing formatted text responses. 
     * The MIME type of the response will be modified, if necessary, to
     * reflect the character encoding used, through the charset=... property. 
     * This means that the content type must be set before calling this 
     * method. 
     * @exception UnsupportedEncodingException if no such encoding can be 
     * provided 
     * @exception IllegalStateException if getOutputStream has been called 
     * on this same request.
     * @exception IOException on other errors. 
     * @see JigsawHttpServletResponse#getOutputStream
     * @see JigsawHttpServletResponse#setContentType 
     */    
    public synchronized PrintWriter getWriter() 
	throws IOException, UnsupportedEncodingException
    {
	if (stream_state == OUTPUT_STREAM_USED)
	    throw new IllegalStateException("Output stream used");
	stream_state = STREAM_WRITER_USED;
      
	if (writer == null) {
	    writer = new PrintWriter(
			 new OutputStreamWriter(getJigsawOutputStream(), 
						getCharacterEncoding()));
	}
	return writer;
    }

    protected synchronized void flushStream(boolean close) 
	throws IOException
    {
	int writeLength;

	if (stream_state == OUTPUT_STREAM_USED) {
	    output.flush();
	} else if (stream_state == STREAM_WRITER_USED) {
	    writer.flush();
	}

	if (request.hasState(INCLUDED)) {
	    if (out == null)
		return;

	    if( fixedContentLength != CALC_CONTENT_LENGTH ) {
		writeLength = (out.size() < fixedContentLength) 
		    ? (out.size())
		    : (fixedContentLength);
	    } else {
		writeLength = out.size();
	    }
	    reply.setContentLength(writeLength);
	    
	    OutputStream rout = reply.getOutputStream(false);
	    byte content[] = out.toByteArray();
	    if (close)
		out.close();
	    else 
		out.reset();
	    rout.write(content);
	    rout.flush();
	}
    }

    JigsawHttpServletResponse(Request request, Reply reply) {
	this.request = request;
	this.reply   = reply;
    }

}
// JigsawServletOutputStream.java
// $Id: JigsawServletOutputStream.java,v 1.8 1999/04/07 08:56:26 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.servlet;

import java.io.*;

import javax.servlet.*;

import org.w3c.jigsaw.http.Reply;

/**
 * This is certainly not a good implementation, it's enough though.
 * (more buffering, better buffering, although the Client output stream
 * will usually come with its own buffering.
 *  @author Alexandre Rafalovitch <alex@access.com.au>
 *  @author Anselm Baird-Smith <abaird@w3.org>
 *  @author Benoît Mahé <bmahe@w3.org>
 */

class JigsawServletOutputStream extends ServletOutputStream {
    DataOutputStream          out   = null;
    JigsawHttpServletResponse resp  = null;
    Reply                     reply = null;

    /**
     * Check that the stream is open, if not open it and send 
     * the headers.
     */
    private void check() 
	throws IOException
    {
	if (out == null) {
	    if (reply != null)
		out = new DataOutputStream(reply.getOutputStream());
	}
    }

    public void print(int i)
	throws IOException
    {
	check();
	out.writeBytes(Integer.toString(i));
    }

    public void print(double i)
	throws IOException
    {
	check();
	out.writeBytes(Double.toString(i));
    }

    public void print(long l)
	throws IOException
    {
	check();
	out.writeBytes(Long.toString(l));
    }

    public void print(String s) 
	throws IOException 
    {
	check();
	out.writeBytes(s);
    }

    public void println() 
	throws IOException
    {
	check();
	out.writeBytes("\r\n");
    }

    public void println(int i)
	throws IOException
    {
	print(i); println();
    }

    public void println(double i)
	throws IOException
    {
	print(i); println();
    }

    public void println(long l) 
	throws IOException
    {
	print(l); println();
    }

    public void println(String s)
	throws IOException
    {
	print(s); println();
    }

    public void write(int b) 
	throws IOException 
    {
	check();
	out.write(b);
    }

    public void write(byte b[]) 
	throws IOException 
    {
	check();
	out.write(b);
    }

    public void write(byte b[], int off, int len) 
	throws IOException 
    {
	check();
	out.write(b, off, len);
    }

    public void flush() 
	throws IOException 
    {
	check();
	out.flush();
    }

    public void close() 
	throws IOException 
    {
	check();
	out.close();
    }

    JigsawServletOutputStream(JigsawHttpServletResponse resp
			      , DataOutputStream out) {
	this.out  = out;
	this.resp = resp;
    }

    JigsawServletOutputStream(JigsawHttpServletResponse resp, Reply reply) {
	this.resp = resp;
	this.reply = reply;
    }

}

Received on Wednesday, 7 April 1999 05:07:25 UTC