/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2010-2014 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.grizzly.http;
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.attributes.Attribute;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.filterchain.NextAction;
import org.glassfish.grizzly.http.util.MimeHeaders;
import org.glassfish.grizzly.memory.MemoryManager;
import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.glassfish.grizzly.ThreadCache;
import org.glassfish.grizzly.http.util.Ascii;
import org.glassfish.grizzly.http.util.Constants;
import org.glassfish.grizzly.http.util.DataChunk;
import org.glassfish.grizzly.http.util.Header;
import static org.glassfish.grizzly.http.util.HttpCodecUtils.*;
/**
* Client side {@link HttpCodecFilter} implementation, which is responsible for
* decoding {@link HttpResponsePacket} and encoding {@link HttpRequestPacket} messages.
*
* This <tt>Filter</tt> is usually used, when we build an asynchronous HTTP client
* connection.
*
* @see HttpCodecFilter
* @see HttpServerFilter
*
* @author Alexey Stashok
*/
public class HttpClientFilter extends HttpCodecFilter {
private final Attribute<Queue<HttpRequestPacket>> httpRequestQueueAttr;
private final Attribute<ClientHttpResponseImpl> httpResponseInProcessAttr;
/**
* Constructor, which creates <tt>HttpClientFilter</tt> instance
*/
public HttpClientFilter() {
this(DEFAULT_MAX_HTTP_PACKET_HEADER_SIZE);
}
/**
* Constructor, which creates <tt>HttpClientFilter</tt> instance,
* with the specific secure and max header size parameter.
*
* @param maxHeadersSize the maximum size of the HTTP message header.
*/
public HttpClientFilter(int maxHeadersSize) {
super(true, maxHeadersSize);
this.httpResponseInProcessAttr =
Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute(
"HttpClientFilter.httpResponse");
this.httpRequestQueueAttr =
Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute(
"HttpClientFilter.httpRequest");
contentEncodings.add(new GZipContentEncoding());
contentEncodings.add(new LZMAContentEncoding());
}
@Override
public NextAction handleWrite(FilterChainContext ctx) throws IOException {
final Connection c = ctx.getConnection();
final Object message = ctx.getMessage();
if (HttpPacket.isHttp(message)) {
assert message instanceof HttpPacket;
final HttpHeader header = ((HttpPacket) message).getHttpHeader();
if (!header.isCommitted() && header.isRequest()) {
assert header instanceof HttpRequestPacket;
getRequestQueue(c).offer((HttpRequestPacket) header);
}
}
return super.handleWrite(ctx);
}
/**
* The method is called, once we have received a {@link Buffer},
* which has to be transformed into HTTP response packet part.
*
* Filter gets {@link Buffer}, which represents a part or complete HTTP
* response message. As the result of "read" transformation - we will get
* {@link HttpContent} message, which will represent HTTP response packet
* content (might be zero length content) and reference
* to a {@link HttpHeader}, which contains HTTP response message header.
*
* @param ctx Request processing context
*
* @return {@link NextAction}
* @throws IOException
*/
@Override
public NextAction handleRead(FilterChainContext ctx) throws IOException {
Buffer input = ctx.getMessage();
final Connection connection = ctx.getConnection();
ClientHttpResponseImpl httpResponse = httpResponseInProcessAttr.get(connection);
if (httpResponse == null) {
httpResponse = ClientHttpResponseImpl.create();
final Queue<HttpRequestPacket> requestQueue = getRequestQueue(connection);
httpResponse.setRequest(requestQueue.poll());
httpResponse.initialize(this, input.position(), maxHeadersSize, MimeHeaders.MAX_NUM_HEADERS_UNBOUNDED);
httpResponse.setSecure(isSecure(connection));
httpResponseInProcessAttr.set(connection, httpResponse);
}
final HttpRequestPacket request = httpResponse.getRequest();
HttpContext httpCtx;
if (request != null) {
httpCtx = request.getProcessingState().getHttpContext();
if (httpCtx == null) {
httpCtx = HttpContext.newInstance(connection,
connection, connection, request);
request.getProcessingState().setHttpContext(httpCtx);
}
} else {
// normally it shouldn't happen, but we use this path in tests to
// check HttpResponsePacket parsing
httpCtx = HttpContext.newInstance(connection,
connection, connection, null);
}
httpCtx.attach(ctx);
return handleRead(ctx, httpResponse);
}
@Override
protected boolean onHttpPacketParsed(HttpHeader httpHeader, FilterChainContext ctx) {
final Connection connection = ctx.getConnection();
clearResponse(connection);
return false;
}
@Override
protected boolean onHttpHeaderParsed(final HttpHeader httpHeader, final Buffer buffer,
final FilterChainContext ctx) {
final ClientHttpResponseImpl response = (ClientHttpResponseImpl) httpHeader;
final HttpRequestPacket request = response.getRequest();
final int statusCode = response.getStatus();
final boolean noContent =
((statusCode == 204) || (statusCode == 205) || (statusCode == 304)
|| ((request != null) && request.isHeadRequest()));
response.setExpectContent(!noContent);
return false;
}
@Override
protected void onHttpHeaderError(final HttpHeader httpHeader,
final FilterChainContext ctx,
final Throwable t) throws IOException {
throw new IllegalStateException(t);
}
@Override
protected void onHttpContentError(final HttpHeader httpHeader,
final FilterChainContext ctx,
final Throwable t) throws IOException {
httpHeader.setContentBroken(true);
throw new IllegalStateException(t);
}
@Override
protected void onInitialLineParsed(final HttpHeader httpHeader,
final FilterChainContext ctx) {
// no-op
}
@Override
protected void onInitialLineEncoded(HttpHeader header, FilterChainContext ctx) {
// no-op
}
@Override
protected void onHttpHeadersParsed(final HttpHeader httpHeader,
final FilterChainContext ctx) {
// no-op
}
@Override
protected void onHttpHeadersEncoded(HttpHeader httpHeader, FilterChainContext ctx) {
// no-op
}
@Override
protected void onHttpContentParsed(HttpContent content, FilterChainContext ctx) {
// no-op
}
@Override
protected void onHttpContentEncoded(HttpContent content, FilterChainContext ctx) {
// no-op
}
protected final void clearResponse(final Connection connection) {
httpResponseInProcessAttr.remove(connection);
}
@Override
protected Buffer encodeHttpPacket(final FilterChainContext ctx,
final HttpPacket input) {
final HttpHeader header;
HttpContent content;
final boolean isHeaderPacket = input.isHeader();
if (isHeaderPacket) {
header = (HttpHeader) input;
content = null;
} else {
content = (HttpContent) input;
header = content.getHttpHeader();
}
final HttpRequestPacket request = (HttpRequestPacket) header;
if (!request.isCommitted()) {
prepareRequest(request);
}
return super.encodeHttpPacket(ctx, header, content, false);
}
@Override
final boolean decodeInitialLineFromBytes(final FilterChainContext ctx,
final HttpPacketParsing httpPacket,
final HeaderParsingState parsingState,
final byte[] input,
final int end) {
final HttpResponsePacket httpResponse = (HttpResponsePacket) httpPacket;
final int arrayOffs = parsingState.arrayOffset;
final int packetLimit = arrayOffs + parsingState.packetLimit;
//noinspection LoopStatementThatDoesntLoop
while(true) {
int subState = parsingState.subState;
switch(subState) {
case 0: { // HTTP protocol
final int spaceIdx =
findSpace(input, arrayOffs + parsingState.offset, end, packetLimit);
if (spaceIdx == -1) {
parsingState.offset = end - arrayOffs;
return false;
}
httpResponse.getProtocolDC().setBytes(input,
arrayOffs + parsingState.start, spaceIdx);
parsingState.start = -1;
parsingState.offset = spaceIdx - arrayOffs;
parsingState.subState++;
}
case 1: { // skip spaces after the HTTP protocol
final int nonSpaceIdx =
skipSpaces(input, arrayOffs + parsingState.offset,
end, packetLimit) - arrayOffs;
if (nonSpaceIdx < 0) {
parsingState.offset = end - arrayOffs;
return false;
}
parsingState.start = nonSpaceIdx;
parsingState.offset = nonSpaceIdx + 1;
parsingState.subState++;
}
case 2 : { // parse the status code
if (parsingState.offset + 3 > end - arrayOffs) {
return false;
}
httpResponse.setStatus(Ascii.parseInt(input,
arrayOffs + parsingState.start,
3));
parsingState.start = -1;
parsingState.offset += 3;
parsingState.subState++;
}
case 3: { // skip spaces after the status code
final int nonSpaceIdx =
skipSpaces(input, arrayOffs + parsingState.offset,
end, packetLimit) - arrayOffs;
if (nonSpaceIdx < 0) {
parsingState.offset = end - arrayOffs;
return false;
}
parsingState.start = nonSpaceIdx;
parsingState.offset = nonSpaceIdx;
parsingState.subState++;
}
case 4: { // HTTP response reason-phrase
if (!findEOL(parsingState, input, end)) {
parsingState.offset = end - arrayOffs;
return false;
}
httpResponse.getReasonPhraseRawDC().setBytes(
input, arrayOffs + parsingState.start,
arrayOffs + parsingState.checkpoint);
parsingState.subState = 0;
parsingState.start = -1;
parsingState.checkpoint = -1;
onInitialLineParsed(httpResponse, ctx);
if (httpResponse.getStatus() == 100) {
// reset the parsing state in preparation to parse
// another initial line which represents the final
// response from the server after it has sent a
// 100-Continue.
parsingState.offset += 2;
parsingState.start = parsingState.offset;
return false;
}
return true;
}
default: throw new IllegalStateException();
}
}
}
@Override
final boolean decodeInitialLineFromBuffer(final FilterChainContext ctx,
final HttpPacketParsing httpPacket,
final HeaderParsingState parsingState,
final Buffer input) {
final HttpResponsePacket httpResponse = (HttpResponsePacket) httpPacket;
final int packetLimit = parsingState.packetLimit;
//noinspection LoopStatementThatDoesntLoop
while(true) {
int subState = parsingState.subState;
switch(subState) {
case 0: { // HTTP protocol
final int spaceIdx =
findSpace(input, parsingState.offset, packetLimit);
if (spaceIdx == -1) {
parsingState.offset = input.limit();
return false;
}
httpResponse.getProtocolDC().setBuffer(input,
parsingState.start, spaceIdx);
parsingState.start = -1;
parsingState.offset = spaceIdx;
parsingState.subState++;
}
case 1: { // skip spaces after the HTTP protocol
final int nonSpaceIdx =
skipSpaces(input, parsingState.offset, packetLimit);
if (nonSpaceIdx == -1) {
parsingState.offset = input.limit();
return false;
}
parsingState.start = nonSpaceIdx;
parsingState.offset = nonSpaceIdx + 1;
parsingState.subState++;
}
case 2 : { // parse the status code
if (parsingState.offset + 3 > input.limit()) {
return false;
}
httpResponse.setStatus(Ascii.parseInt(input,
parsingState.start,
3));
parsingState.start = -1;
parsingState.offset += 3;
parsingState.subState++;
}
case 3: { // skip spaces after the status code
final int nonSpaceIdx =
skipSpaces(input, parsingState.offset, packetLimit);
if (nonSpaceIdx == -1) {
parsingState.offset = input.limit();
return false;
}
parsingState.start = nonSpaceIdx;
parsingState.offset = nonSpaceIdx;
parsingState.subState++;
}
case 4: { // HTTP response reason-phrase
if (!findEOL(parsingState, input)) {
parsingState.offset = input.limit();
return false;
}
httpResponse.getReasonPhraseRawDC().setBuffer(
input, parsingState.start,
parsingState.checkpoint);
parsingState.subState = 0;
parsingState.start = -1;
parsingState.checkpoint = -1;
onInitialLineParsed(httpResponse, ctx);
if (httpResponse.getStatus() == 100) {
// reset the parsing state in preparation to parse
// another initial line which represents the final
// response from the server after it has sent a
// 100-Continue.
parsingState.offset += 2;
parsingState.start = 0;
input.position(parsingState.offset);
input.shrink();
parsingState.offset = 0;
return false;
}
return true;
}
default: throw new IllegalStateException();
}
}
}
@Override
Buffer encodeInitialLine(HttpPacket httpPacket, Buffer output, MemoryManager memoryManager) {
final HttpRequestPacket httpRequest = (HttpRequestPacket) httpPacket;
final byte[] tempEncodingBuffer = httpRequest.getTempHeaderEncodingBuffer();
output = put(memoryManager, output, tempEncodingBuffer, httpRequest.getMethodDC());
output = put(memoryManager, output, Constants.SP);
output = put(memoryManager, output, tempEncodingBuffer, httpRequest.getRequestURIRef().getRequestURIBC());
if (!httpRequest.getQueryStringDC().isNull()) {
output = put(memoryManager, output, (byte) '?');
output = put(memoryManager, output, tempEncodingBuffer, httpRequest.getQueryStringDC());
}
output = put(memoryManager, output, Constants.SP);
output = put(memoryManager, output, tempEncodingBuffer, httpRequest.getProtocolString());
return output;
}
private void prepareRequest(final HttpRequestPacket request) {
String contentType = request.getContentType();
if (contentType != null) {
request.getHeaders().setValue(Header.ContentType).setString(contentType);
}
}
private Queue<HttpRequestPacket> getRequestQueue(final Connection c) {
Queue<HttpRequestPacket> q = httpRequestQueueAttr.get(c);
if (q == null) {
q = new ConcurrentLinkedQueue<HttpRequestPacket>();
httpRequestQueueAttr.set(c, q);
}
return q;
}
private static final class ClientHttpResponseImpl
extends HttpResponsePacket implements HttpPacketParsing {
private static final ThreadCache.CachedTypeIndex<ClientHttpResponseImpl> CACHE_IDX =
ThreadCache.obtainIndex(ClientHttpResponseImpl.class, 16);
public static ClientHttpResponseImpl create() {
final ClientHttpResponseImpl httpResponseImpl =
ThreadCache.takeFromCache(CACHE_IDX);
if (httpResponseImpl != null) {
return httpResponseImpl;
}
return new ClientHttpResponseImpl();
}
/**
* content type parsed flag.
*/
private boolean contentTypeParsed;
private boolean isHeaderParsed;
private final HttpCodecFilter.HeaderParsingState headerParsingState;
private final HttpCodecFilter.ContentParsingState contentParsingState;
private ClientHttpResponseImpl() {
this.headerParsingState = new HttpCodecFilter.HeaderParsingState();
this.contentParsingState = new HttpCodecFilter.ContentParsingState();
}
public void initialize(final HttpCodecFilter filter,
final int initialOffset,
final int maxHeaderSize,
final int maxNumberOfHeaders) {
headerParsingState.initialize(filter, initialOffset, maxHeaderSize);
headers.setMaxNumHeaders(maxNumberOfHeaders);
contentParsingState.trailerHeaders.setMaxNumHeaders(maxNumberOfHeaders);
}
@Override
public String getCharacterEncoding() {
if (!contentTypeParsed) {
parseContentTypeHeader();
}
return super.getCharacterEncoding();
}
@Override
public void setCharacterEncoding(final String charset) {
if (!contentTypeParsed) {
parseContentTypeHeader();
}
super.setCharacterEncoding(charset);
}
@Override
public String getContentType() {
if (!contentTypeParsed) {
parseContentTypeHeader();
}
return super.getContentType();
}
private void parseContentTypeHeader() {
contentTypeParsed = true;
if (!contentType.isSet()) {
final DataChunk dc = headers.getValue(Header.ContentType);
if (dc != null && !dc.isNull()) {
setContentType(dc.toString());
}
}
}
@Override
public HttpCodecFilter.HeaderParsingState getHeaderParsingState() {
return headerParsingState;
}
@Override
public ContentParsingState getContentParsingState() {
return contentParsingState;
}
@Override
public ProcessingState getProcessingState() {
return getRequest().getProcessingState();
}
@Override
public boolean isHeaderParsed() {
return isHeaderParsed;
}
@Override
public void setHeaderParsed(boolean isHeaderParsed) {
this.isHeaderParsed = isHeaderParsed;
}
/**
* {@inheritDoc}
*/
@Override
protected void reset() {
contentTypeParsed = false;
isHeaderParsed = false;
headerParsingState.recycle();
contentParsingState.recycle();
super.reset();
}
/**
* {@inheritDoc}
*/
@Override
public void recycle() {
if (getRequest().isExpectContent()) {
return;
}
reset();
ThreadCache.putToCache(CACHE_IDX, this);
}
}
}