/*
* 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.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutorGroup;
import rx.Observable;
import rx.Observable.Transformer;
import rx.Subscriber;
import rx.functions.Action1;
import rx.functions.Func1;
/**
* An abstraction over netty's channel providing Rx APIs.
*
* <h2>Reading data</h2>
*
* Unless, {@link ChannelOption#AUTO_READ} is set to {@code true} on the underneath channel, data will be read from the
* connection if and only if there is a subscription to the input stream returned by {@link #getInput()}.
* In case, the input data is not required to be consumed, one should call {@link #ignoreInput()}, otherwise, data will
* never be read from the channel.
*
* @param <R> Type of object that is read from this connection.
* @param <W> Type of object that is written to this connection.
*/
public abstract class Connection<R, W> implements ChannelOperations<W> {
public static final AttributeKey<Connection> CONNECTION_ATTRIBUTE_KEY = AttributeKey.valueOf("rx-netty-conn-attr");
private final Channel nettyChannel;
private final ContentSource<R> contentSource;
protected final MarkAwarePipeline markAwarePipeline;
protected Connection(final Channel nettyChannel) {
if (null == nettyChannel) {
throw new IllegalArgumentException("Channel can not be null");
}
this.nettyChannel = nettyChannel;
markAwarePipeline = new MarkAwarePipeline(nettyChannel.pipeline());
contentSource = new ContentSource<>(nettyChannel, new Func1<Subscriber<? super R>, Object>() {
@Override
public Object call(Subscriber<? super R> subscriber) {
return new ConnectionInputSubscriberEvent<>(subscriber);
}
});
}
protected Connection(Connection<R, W> toCopy) {
nettyChannel = toCopy.nettyChannel;
markAwarePipeline = toCopy.markAwarePipeline;
contentSource = toCopy.contentSource;
}
protected Connection(Connection<?, ?> toCopy, ContentSource<R> contentSource) {
nettyChannel = toCopy.nettyChannel;
markAwarePipeline = toCopy.markAwarePipeline;
this.contentSource = contentSource;
}
/**
* Returns a stream of data that is read from the connection.
*
* Unless, {@link ChannelOption#AUTO_READ} is set to {@code true}, the content will only be read from the
* underneath channel, if there is a subscriber to the input.
* In case, input is not required to be read, call {@link #ignoreInput()}
*
* @return The stream of data that is read from the connection.
*/
public ContentSource<R> getInput() {
return contentSource;
}
/**
* Ignores all input on this connection.
*
* Unless, {@link ChannelOption#AUTO_READ} is set to {@code true}, the content will only be read from the
* underneath channel, if there is a subscriber to the input. So, upon recieving this connection, either one should
* call this method or eventually subscribe to the stream returned by {@link #getInput()}
*
* @return An {@link Observable}, subscription to which will discard the input. This {@code Observable} will
* error/complete when the input errors/completes and unsubscription from here will unsubscribe from the content.
*/
public Observable<Void> ignoreInput() {
return getInput().map(new Func1<R, Void>() {
@Override
public Void call(R r) {
ReferenceCountUtil.release(r);
return null;
}
}).ignoreElements();
}
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the first position of the pipeline as specified by {@link ChannelPipeline#addFirst(String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerFirst(String name, ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the first position of the pipeline as specified by
* {@link ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerFirst(EventExecutorGroup group, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the last position of the pipeline as specified by {@link ChannelPipeline#addLast(String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerLast(String name, ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the last position of the pipeline as specified by
* {@link ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerLast(EventExecutorGroup group, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addBefore(String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param baseName the name of the existing handler
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerBefore(String baseName, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler}
* methods
* @param baseName the name of the existing handler
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerBefore(EventExecutorGroup group, String baseName,
String name, ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addAfter(String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param baseName the name of the existing handler
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerAfter(String baseName, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods
* @param baseName the name of the existing handler
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerAfter(EventExecutorGroup group, String baseName,
String name, ChannelHandler handler);
/**
* Configures the {@link ChannelPipeline} for this channel, using the passed {@code pipelineConfigurator}.
*
* @param pipelineConfigurator Action to configure {@link ChannelPipeline}.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> pipelineConfigurator(Action1<ChannelPipeline> pipelineConfigurator);
/**
* Transforms this connection's input stream using the passed {@code transformer} to create a new
* {@code Connection} instance.
*
* @param transformer Transformer to transform the input stream.
*
* @param <RR> New type of the input stream.
*
* @return A new connection instance with the transformed read stream.
*/
public abstract <RR> Connection<RR, W> transformRead(Transformer<R, RR> transformer);
/**
* Transforms this connection to enable writing a different object type.
*
* @param transformer Transformer to transform objects written to the channel.
*
* @param <WW> New object types to be written to the connection.
*
* @return A new connection instance with the new write type.
*/
public abstract <WW> Connection<R, WW> transformWrite(AllocatingTransformer<WW, W> transformer);
/**
* Returns the {@link MarkAwarePipeline} for this connection, changes to which can be reverted at any point in time.
*/
public MarkAwarePipeline getResettableChannelPipeline() {
return markAwarePipeline;
}
/**
* Returns the {@link ChannelPipeline} for this connection.
*
* @return {@link ChannelPipeline} for this connection.
*/
public ChannelPipeline getChannelPipeline() {
return nettyChannel.pipeline();
}
/**
* Returns the underlying netty {@link Channel} for this connection.
*
* <h2>Why unsafe?</h2>
*
* It is advisable to use this connection abstraction for all interactions with the channel, however, advanced users
* may find directly using the netty channel useful in some cases.
*
* @return The underlying netty {@link Channel} for this connection.
*/
public Channel unsafeNettyChannel() {
return nettyChannel;
}
/*
* In order to make sure that the connection is correctly initialized, the listener needs to be added post
* constructor. Otherwise, there is a race-condition of the channel closed before the connection is completely
* created and the Connection.close() call on channel close can access the Connection object which isn't
* constructed completely. IOW, "this" escapes from the constructor if the listener is added in the constructor.
*/
protected void connectCloseToChannelClose() {
nettyChannel.closeFuture()
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
close(false); // Close this connection when the channel is closed.
}
});
nettyChannel.attr(CONNECTION_ATTRIBUTE_KEY).set(this);
}
}