All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.server.handler.BufferedResponseHandler Maven / Gradle / Ivy

There is a newer version: 12.0.13
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2019 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.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 { final ServletContext context = baseRequest.getServletContext(); final 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 - 2024 Weber Informatics LLC | Privacy Policy