/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.protocol.http.client.internal;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.util.ReferenceCountUtil;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.channel.ContentSource;
import io.reactivex.netty.protocol.http.CookiesHolder;
import io.reactivex.netty.protocol.http.HttpHandlerNames;
import io.reactivex.netty.protocol.http.client.HttpClientResponse;
import io.reactivex.netty.protocol.http.internal.HttpContentSubscriberEvent;
import io.reactivex.netty.protocol.http.sse.ServerSentEvent;
import io.reactivex.netty.protocol.http.sse.client.ServerSentEventDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.Observable.Transformer;
import rx.Subscriber;
import rx.functions.Func1;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import static io.netty.handler.codec.http.HttpHeaderNames.*;
public final class HttpClientResponseImpl<T> extends HttpClientResponse<T> {
private static final Logger logger = LoggerFactory.getLogger(HttpClientResponseImpl.class);
public static final String KEEP_ALIVE_HEADER_NAME = "Keep-Alive";
private static final Pattern PATTERN_COMMA = Pattern.compile(",");
private static final Pattern PATTERN_EQUALS = Pattern.compile("=");
public static final String KEEP_ALIVE_TIMEOUT_HEADER_ATTR = "timeout";
private final HttpResponse nettyResponse;
private final Connection<?, ?> connection;
private final CookiesHolder cookiesHolder;
private final ContentSource<T> contentSource;
private HttpClientResponseImpl(HttpResponse nettyResponse) {
this(nettyResponse, UnusableConnection.create());
}
private HttpClientResponseImpl(HttpResponse nettyResponse, Connection<?, ?> connection) {
this.nettyResponse = nettyResponse;
this.connection = connection;
cookiesHolder = CookiesHolder.newClientResponseHolder(nettyResponse.headers());
contentSource = new ContentSource<>(unsafeNettyChannel(), new ContentSourceSubscriptionFactory<T>());
}
private HttpClientResponseImpl(HttpClientResponseImpl<?> toCopy, ContentSource<T> newSource) {
nettyResponse = toCopy.nettyResponse;
connection = toCopy.connection;
cookiesHolder = toCopy.cookiesHolder;
contentSource = newSource;
}
@Override
public HttpVersion getHttpVersion() {
return nettyResponse.protocolVersion();
}
@Override
public HttpResponseStatus getStatus() {
return nettyResponse.status();
}
@Override
public Map<String, Set<Cookie>> getCookies() {
return cookiesHolder.getAllCookies();
}
@Override
public boolean containsHeader(CharSequence name) {
return nettyResponse.headers().contains(name);
}
@Override
public boolean containsHeader(CharSequence name, CharSequence value, boolean ignoreCaseValue) {
return nettyResponse.headers().contains(name, value, ignoreCaseValue);
}
@Override
public Iterator<Entry<CharSequence, CharSequence>> headerIterator() {
return nettyResponse.headers().iteratorCharSequence();
}
@Override
public String getHeader(CharSequence name) {
return nettyResponse.headers().get(name);
}
@Override
public String getHeader(CharSequence name, String defaultValue) {
return nettyResponse.headers().get(name, defaultValue);
}
@Override
public List<String> getAllHeaderValues(CharSequence name) {
return nettyResponse.headers().getAll(name);
}
@Override
public long getContentLength() {
return HttpUtil.getContentLength(nettyResponse);
}
@Override
public long getContentLength(long defaultValue) {
return HttpUtil.getContentLength(nettyResponse, defaultValue);
}
@Override
public long getDateHeader(CharSequence name) {
return nettyResponse.headers().getTimeMillis(name);
}
@Override
public long getDateHeader(CharSequence name, long defaultValue) {
return nettyResponse.headers().getTimeMillis(name, defaultValue);
}
@Override
public String getHostHeader() {
return nettyResponse.headers().get(HOST);
}
@Override
public String getHost(String defaultValue) {
return nettyResponse.headers().get(HOST, defaultValue);
}
@Override
public int getIntHeader(CharSequence name) {
return nettyResponse.headers().getInt(name);
}
@Override
public int getIntHeader(CharSequence name, int defaultValue) {
return nettyResponse.headers().getInt(name, defaultValue);
}
@Override
public boolean isContentLengthSet() {
return HttpUtil.isContentLengthSet(nettyResponse);
}
@Override
public boolean isKeepAlive() {
return HttpUtil.isKeepAlive(nettyResponse);
}
@Override
public boolean isTransferEncodingChunked() {
return HttpUtil.isTransferEncodingChunked(nettyResponse);
}
@Override
public Set<String> getHeaderNames() {
return nettyResponse.headers().names();
}
@Override
public HttpClientResponse<T> addHeader(CharSequence name, Object value) {
nettyResponse.headers().add(name, value);
return this;
}
@Override
public HttpClientResponse<T> addCookie(Cookie cookie) {
nettyResponse.headers().add(SET_COOKIE, ClientCookieEncoder.STRICT.encode(cookie));
return this;
}
@Override
public HttpClientResponse<T> addDateHeader(CharSequence name, Date value) {
nettyResponse.headers().set(name, value);
return this;
}
@Override
public HttpClientResponse<T> addDateHeader(CharSequence name, Iterable<Date> values) {
for (Date value : values) {
nettyResponse.headers().add(name, value);
}
return this;
}
@Override
public HttpClientResponse<T> addHeader(CharSequence name, Iterable<Object> values) {
nettyResponse.headers().add(name, values);
return this;
}
@Override
public HttpClientResponse<T> setDateHeader(CharSequence name, Date value) {
nettyResponse.headers().set(name, value);
return this;
}
@Override
public HttpClientResponse<T> setHeader(CharSequence name, Object value) {
nettyResponse.headers().set(name, value);
return this;
}
@Override
public HttpClientResponse<T> setDateHeader(CharSequence name, Iterable<Date> values) {
for (Date value : values) {
nettyResponse.headers().set(name, value);
}
return this;
}
@Override
public HttpClientResponse<T> setHeader(CharSequence name, Iterable<Object> values) {
nettyResponse.headers().set(name, values);
return this;
}
@Override
public HttpClientResponse<T> removeHeader(CharSequence name) {
nettyResponse.headers().remove(name);
return this;
}
@Override
public ContentSource<ServerSentEvent> getContentAsServerSentEvents() {
if (containsHeader(CONTENT_TYPE) && getHeader(CONTENT_TYPE).startsWith("text/event-stream")) {
ChannelPipeline pipeline = unsafeNettyChannel().pipeline();
ChannelHandlerContext decoderCtx = pipeline.context(HttpHandlerNames.HttpClientCodec.getName());
if (null != decoderCtx) {
pipeline.addAfter(decoderCtx.name(), HttpHandlerNames.SseClientCodec.getName(),
new ServerSentEventDecoder());
}
return new ContentSource<>(unsafeNettyChannel(), new ContentSourceSubscriptionFactory<ServerSentEvent>());
}
return new ContentSource<>(new IllegalStateException("Response is not a server sent event response."));
}
@Override
public ContentSource<T> getContent() {
return contentSource;
}
@Override
public Observable<Void> discardContent() {
return getContent().map(new Func1<T, Void>() {
@Override
public Void call(T t) {
ReferenceCountUtil.release(t);
return null;
}
}).ignoreElements();
}
@Override
public <TT> HttpClientResponse<TT> transformContent(Transformer<T, TT> transformer) {
return new HttpClientResponseImpl<>(this, contentSource.transform(transformer));
}
@Override
public Channel unsafeNettyChannel() {
return unsafeConnection().unsafeNettyChannel();
}
@Override
public Connection<?, ?> unsafeConnection() {
return connection;
}
/**
* Parses the timeout value from the HTTP keep alive header (with name {@link #KEEP_ALIVE_HEADER_NAME}) as described in
* <a href="http://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html">this spec</a>
*
* @return The keep alive timeout or {@code null} if this response does not define the appropriate header value.
*/
public Long getKeepAliveTimeoutSeconds() {
String keepAliveHeader = nettyResponse.headers().get(KEEP_ALIVE_HEADER_NAME);
if (null != keepAliveHeader && !keepAliveHeader.isEmpty()) {
String[] pairs = PATTERN_COMMA.split(keepAliveHeader);
if (pairs != null) {
for (String pair: pairs) {
String[] nameValue = PATTERN_EQUALS.split(pair.trim());
if (nameValue != null && nameValue.length >= 2) {
String name = nameValue[0].trim().toLowerCase();
String value = nameValue[1].trim();
if (KEEP_ALIVE_TIMEOUT_HEADER_ATTR.equals(name)) {
try {
return Long.valueOf(value);
} catch (NumberFormatException e) {
logger.info("Invalid HTTP keep alive timeout value. Keep alive header: "
+ keepAliveHeader + ", timeout attribute value: " + nameValue[1], e);
return null;
}
}
}
}
}
}
return null;
}
/*Visible for the client bridge*/static <C> HttpClientResponseImpl<C> unsafeCreate(HttpResponse nettyResponse) {
return new HttpClientResponseImpl<>(nettyResponse);
}
public static <C> HttpClientResponse<C> newInstance(HttpClientResponse<C> unsafeInstance,
Connection<?, ?> connection) {
HttpClientResponseImpl<C> cast = (HttpClientResponseImpl<C>) unsafeInstance;
return new HttpClientResponseImpl<>(cast.nettyResponse, connection);
}
public static <C> HttpClientResponse<C> newInstance(HttpResponse nettyResponse, Connection<?, ?> connection) {
return new HttpClientResponseImpl<>(nettyResponse, connection);
}
private static class ContentSourceSubscriptionFactory<T> implements Func1<Subscriber<? super T>, Object> {
@Override
public Object call(Subscriber<? super T> subscriber) {
return new HttpContentSubscriberEvent<>(subscriber);
}
}
}