org.eclipse.jetty.server.handler.BufferedResponseHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ehcache Show documentation
Show all versions of ehcache Show documentation
Ehcache is an open source, standards-based cache used to boost performance,
offload the database and simplify scalability. Ehcache is robust, proven and full-featured and
this has made it the most widely-used Java-based cache.
//
// ========================================================================
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.HttpOutput.Interceptor;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IncludeExclude;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Buffered Response Handler
*
* A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
* mechanism to buffer the entire response content until the output is closed.
* This allows the commit to be delayed until the response is complete and thus
* headers and response status can be changed while writing the body.
*
* Note that the decision to buffer is influenced by the headers and status at the
* first write, and thus subsequent changes to those headers will not influence the
* decision to buffer or not.
*
* Note also that there are no memory limits to the size of the buffer, thus
* this handler can represent an unbounded memory commitment if the content
* generated can also be unbounded.
*
*/
public class BufferedResponseHandler extends HandlerWrapper
{
static final Logger LOG = Log.getLogger(BufferedResponseHandler.class);
private final IncludeExclude _methods = new IncludeExclude<>();
private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude _mimeTypes = new IncludeExclude<>();
/* ------------------------------------------------------------ */
public BufferedResponseHandler()
{
// include only GET requests
_methods.include(HttpMethod.GET.asString());
// Exclude images, aduio and video from buffering
for (String type:MimeTypes.getKnownMimeTypes())
{
if (type.startsWith("image/")||
type.startsWith("audio/")||
type.startsWith("video/"))
_mimeTypes.exclude(type);
}
LOG.debug("{} mime types {}",this,_mimeTypes);
}
/* ------------------------------------------------------------ */
public IncludeExclude getMethodIncludeExclude()
{
return _methods;
}
/* ------------------------------------------------------------ */
public IncludeExclude getPathIncludeExclude()
{
return _paths;
}
/* ------------------------------------------------------------ */
public IncludeExclude getMimeIncludeExclude()
{
return _mimeTypes;
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
ServletContext context = baseRequest.getServletContext();
String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
LOG.debug("{} handle {} in {}",this,baseRequest,context);
HttpOutput out = baseRequest.getResponse().getHttpOutput();
// Are we already being gzipped?
HttpOutput.Interceptor interceptor = out.getInterceptor();
while (interceptor!=null)
{
if (interceptor instanceof BufferedInterceptor)
{
LOG.debug("{} already intercepting {}",this,request);
_handler.handle(target,baseRequest, request, response);
return;
}
interceptor=interceptor.getNextInterceptor();
}
// If not a supported method - no Vary because no matter what client, this URI is always excluded
if (!_methods.test(baseRequest.getMethod()))
{
LOG.debug("{} excluded by method {}",this,request);
_handler.handle(target,baseRequest, request, response);
return;
}
// If not a supported URI- no Vary because no matter what client, this URI is always excluded
// Use pathInfo because this is be
if (!isPathBufferable(path))
{
LOG.debug("{} excluded by path {}",this,request);
_handler.handle(target,baseRequest, request, response);
return;
}
// If the mime type is known from the path, then apply mime type filtering
String mimeType = context==null?MimeTypes.getDefaultMimeByExtension(path):context.getMimeType(path);
if (mimeType!=null)
{
mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
if (!isMimeTypeBufferable(mimeType))
{
LOG.debug("{} excluded by path suffix mime type {}",this,request);
// handle normally without setting vary header
_handler.handle(target,baseRequest, request, response);
return;
}
}
// install interceptor and handle
out.setInterceptor(new BufferedInterceptor(baseRequest.getHttpChannel(),out.getInterceptor()));
if (_handler!=null)
_handler.handle(target,baseRequest, request, response);
}
/* ------------------------------------------------------------ */
protected boolean isMimeTypeBufferable(String mimetype)
{
return _mimeTypes.test(mimetype);
}
/* ------------------------------------------------------------ */
protected boolean isPathBufferable(String requestURI)
{
if (requestURI == null)
return true;
return _paths.test(requestURI);
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
private class BufferedInterceptor implements HttpOutput.Interceptor
{
final Interceptor _next;
final HttpChannel _channel;
final Queue _buffers=new ConcurrentLinkedQueue<>();
Boolean _aggregating;
ByteBuffer _aggregate;
public BufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
_next=interceptor;
_channel=httpChannel;
}
@Override
public void resetBuffer()
{
_buffers.clear();
_aggregating=null;
_aggregate=null;
};
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} write last={} {}",this,last,BufferUtil.toDetailString(content));
// if we are not committed, have to decide if we should aggregate or not
if (_aggregating==null)
{
Response response = _channel.getResponse();
int sc = response.getStatus();
if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
_aggregating=Boolean.FALSE; // No body
else
{
String ct = response.getContentType();
if (ct==null)
_aggregating=Boolean.TRUE;
else
{
ct=MimeTypes.getContentTypeWithoutCharset(ct);
_aggregating=isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
}
}
}
// If we are not aggregating, then handle normally
if (!_aggregating.booleanValue())
{
getNextInterceptor().write(content,last,callback);
return;
}
// If last
if (last)
{
// Add the current content to the buffer list without a copy
if (BufferUtil.length(content)>0)
_buffers.add(content);
if (LOG.isDebugEnabled())
LOG.debug("{} committing {}",this,_buffers.size());
commit(_buffers,callback);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("{} aggregating",this);
// Aggregate the content into buffer chain
while (BufferUtil.hasContent(content))
{
// Do we need a new aggregate buffer
if (BufferUtil.space(_aggregate)==0)
{
int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(),BufferUtil.length(content));
_aggregate=BufferUtil.allocate(size); // TODO use a buffer pool
_buffers.add(_aggregate);
}
BufferUtil.append(_aggregate,content);
}
callback.succeeded();
}
}
@Override
public Interceptor getNextInterceptor()
{
return _next;
}
@Override
public boolean isOptimizedForDirectBuffers()
{
return false;
}
protected void commit(Queue buffers, Callback callback)
{
// If only 1 buffer
if (_buffers.size()==0)
getNextInterceptor().write(BufferUtil.EMPTY_BUFFER,true,callback);
else if (_buffers.size()==1)
// just flush it with the last callback
getNextInterceptor().write(_buffers.remove(),true,callback);
else
{
// Create an iterating callback to do the writing
IteratingCallback icb = new IteratingCallback()
{
@Override
protected Action process() throws Exception
{
ByteBuffer buffer = _buffers.poll();
if (buffer==null)
return Action.SUCCEEDED;
getNextInterceptor().write(buffer,_buffers.isEmpty(),this);
return Action.SCHEDULED;
}
@Override
protected void onCompleteSuccess()
{
// Signal last callback
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
// Signal last callback
callback.failed(cause);
}
};
icb.iterate();
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy