/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.camel.component.netty4; import java.io.File; import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.handler.codec.Delimiters; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; import org.apache.camel.Exchange; import org.apache.camel.LoggingLevel; import org.apache.camel.RuntimeCamelException; import org.apache.camel.spi.UriParam; import org.apache.camel.spi.UriParams; import org.apache.camel.util.EndpointHelper; import org.apache.camel.util.IntrospectionSupport; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @UriParams public class NettyConfiguration extends NettyServerBootstrapConfiguration implements Cloneable { private static final Logger LOG = LoggerFactory.getLogger(NettyConfiguration.class); @UriParam(label = "producer") private long requestTimeout; @UriParam(defaultValue = "true") private boolean sync = true; @UriParam(label = "codec") private boolean textline; @UriParam(label = "codec", defaultValue = "LINE") private TextLineDelimiter delimiter = TextLineDelimiter.LINE; @UriParam(label = "codec", defaultValue = "true") private boolean autoAppendDelimiter = true; @UriParam(label = "codec", defaultValue = "1024") private int decoderMaxLineLength = 1024; @UriParam(label = "codec") private String encoding; @UriParam(label = "codec", description = "To use a single encoder. This options is deprecated use encoders instead.") @Deprecated private ChannelHandler encoder; @UriParam(label = "codec", javaType = "java.lang.String") private List<ChannelHandler> encoders = new ArrayList<ChannelHandler>(); @UriParam(label = "codec", description = "To use a single decoder. This options is deprecated use encoders instead.") @Deprecated private ChannelHandler decoder; @UriParam(label = "codec", javaType = "java.lang.String") private List<ChannelHandler> decoders = new ArrayList<ChannelHandler>(); @UriParam private boolean disconnect; @UriParam(label = "producer,advanced", defaultValue = "true") private boolean lazyChannelCreation = true; @UriParam(label = "advanced") private boolean transferExchange; @UriParam(label = "advanced", defaultValue = "false") private boolean allowSerializedHeaders; @UriParam(label = "consumer,advanced", defaultValue = "true") private boolean disconnectOnNoReply = true; @UriParam(label = "consumer,advanced", defaultValue = "WARN") private LoggingLevel noReplyLogLevel = LoggingLevel.WARN; @UriParam(label = "consumer,advanced", defaultValue = "WARN") private LoggingLevel serverExceptionCaughtLogLevel = LoggingLevel.WARN; @UriParam(label = "consumer,advanced", defaultValue = "DEBUG") private LoggingLevel serverClosedChannelExceptionCaughtLogLevel = LoggingLevel.DEBUG; @UriParam(label = "codec", defaultValue = "true") private boolean allowDefaultCodec = true; @UriParam(label = "producer,advanced") private ClientInitializerFactory clientInitializerFactory; @UriParam(label = "consumer,advanced", defaultValue = "true") private boolean usingExecutorService = true; @UriParam(label = "producer,advanced", defaultValue = "-1") private int producerPoolMaxActive = -1; @UriParam(label = "producer,advanced") private int producerPoolMinIdle; @UriParam(label = "producer,advanced", defaultValue = "100") private int producerPoolMaxIdle = 100; @UriParam(label = "producer,advanced", defaultValue = "" + 5 * 60 * 1000L) private long producerPoolMinEvictableIdle = 5 * 60 * 1000L; @UriParam(label = "producer,advanced", defaultValue = "true") private boolean producerPoolEnabled = true; @UriParam(label = "producer,advanced") private boolean udpConnectionlessSending; @UriParam(label = "consumer") private boolean clientMode; @UriParam(label = "producer,advanced") private boolean useByteBuf; @UriParam(label = "advanced") private boolean udpByteArrayCodec; @UriParam(label = "producer") private boolean reuseChannel; /** * Returns a copy of this configuration */ public NettyConfiguration copy() { try { NettyConfiguration answer = (NettyConfiguration) clone(); // make sure the lists is copied in its own instance List<ChannelHandler> encodersCopy = new ArrayList<ChannelHandler>(encoders); answer.setEncoders(encodersCopy); List<ChannelHandler> decodersCopy = new ArrayList<ChannelHandler>(decoders); answer.setDecoders(decodersCopy); return answer; } catch (CloneNotSupportedException e) { throw new RuntimeCamelException(e); } } public void validateConfiguration() { // validate that the encoders is either shareable or is a handler factory for (ChannelHandler encoder : encoders) { if (encoder instanceof ChannelHandlerFactory) { continue; } if (ObjectHelper.getAnnotation(encoder, ChannelHandler.Sharable.class) != null) { continue; } LOG.warn("The encoder {} is not @Shareable or an ChannelHandlerFactory instance. The encoder cannot safely be used.", encoder); } // validate that the decoders is either shareable or is a handler factory for (ChannelHandler decoder : decoders) { if (decoder instanceof ChannelHandlerFactory) { continue; } if (ObjectHelper.getAnnotation(decoder, ChannelHandler.Sharable.class) != null) { continue; } LOG.warn("The decoder {} is not @Shareable or an ChannelHandlerFactory instance. The decoder cannot safely be used.", decoder); } if (sslHandler != null) { boolean factory = sslHandler instanceof ChannelHandlerFactory; boolean shareable = ObjectHelper.getAnnotation(sslHandler, ChannelHandler.Sharable.class) != null; if (!factory && !shareable) { LOG.warn("The sslHandler {} is not @Shareable or an ChannelHandlerFactory instance. The sslHandler cannot safely be used.", sslHandler); } } } public void parseURI(URI uri, Map<String, Object> parameters, NettyComponent component, String... supportedProtocols) throws Exception { protocol = uri.getScheme(); boolean found = false; for (String supportedProtocol : supportedProtocols) { if (protocol != null && protocol.equalsIgnoreCase(supportedProtocol)) { found = true; break; } } if (!found) { throw new IllegalArgumentException("Unrecognized Netty protocol: " + protocol + " for uri: " + uri); } setHost(uri.getHost()); if (uri.getPort() != -1) { setPort(uri.getPort()); } ssl = component.getAndRemoveOrResolveReferenceParameter(parameters, "ssl", boolean.class, false); sslHandler = component.getAndRemoveOrResolveReferenceParameter(parameters, "sslHandler", SslHandler.class, sslHandler); passphrase = component.getAndRemoveOrResolveReferenceParameter(parameters, "passphrase", String.class, passphrase); keyStoreFormat = component.getAndRemoveOrResolveReferenceParameter(parameters, "keyStoreFormat", String.class, keyStoreFormat == null ? "JKS" : keyStoreFormat); securityProvider = component.getAndRemoveOrResolveReferenceParameter(parameters, "securityProvider", String.class, securityProvider == null ? "SunX509" : securityProvider); keyStoreFile = component.getAndRemoveOrResolveReferenceParameter(parameters, "keyStoreFile", File.class, keyStoreFile); trustStoreFile = component.getAndRemoveOrResolveReferenceParameter(parameters, "trustStoreFile", File.class, trustStoreFile); keyStoreResource = component.getAndRemoveOrResolveReferenceParameter(parameters, "keyStoreResource", String.class, keyStoreResource); trustStoreResource = component.getAndRemoveOrResolveReferenceParameter(parameters, "trustStoreResource", String.class, trustStoreResource); // clientPipelineFactory is @deprecated and to be removed clientInitializerFactory = component.getAndRemoveOrResolveReferenceParameter(parameters, "clientPipelineFactory", ClientInitializerFactory.class, clientInitializerFactory); clientInitializerFactory = component.getAndRemoveOrResolveReferenceParameter(parameters, "clientInitializerFactory", ClientInitializerFactory.class, clientInitializerFactory); // serverPipelineFactory is @deprecated and to be removed serverInitializerFactory = component.getAndRemoveOrResolveReferenceParameter(parameters, "serverPipelineFactory", ServerInitializerFactory.class, serverInitializerFactory); serverInitializerFactory = component.getAndRemoveOrResolveReferenceParameter(parameters, "serverInitializerFactory", ServerInitializerFactory.class, serverInitializerFactory); // set custom encoders and decoders first List<ChannelHandler> referencedEncoders = component.resolveAndRemoveReferenceListParameter(parameters, "encoders", ChannelHandler.class, null); addToHandlersList(encoders, referencedEncoders, ChannelHandler.class); List<ChannelHandler> referencedDecoders = component.resolveAndRemoveReferenceListParameter(parameters, "decoders", ChannelHandler.class, null); addToHandlersList(decoders, referencedDecoders, ChannelHandler.class); // then set parameters with the help of the camel context type converters EndpointHelper.setReferenceProperties(component.getCamelContext(), this, parameters); EndpointHelper.setProperties(component.getCamelContext(), this, parameters); // additional netty options, we don't want to store an empty map, so set it as null if empty options = IntrospectionSupport.extractProperties(parameters, "option."); if (options != null && options.isEmpty()) { options = null; } // add default encoders and decoders if (encoders.isEmpty() && decoders.isEmpty()) { if (isAllowDefaultCodec()) { if ("udp".equalsIgnoreCase(protocol)) { encoders.add(ChannelHandlerFactories.newDatagramPacketEncoder()); } // are we textline or object? if (isTextline()) { Charset charset = getEncoding() != null ? Charset.forName(getEncoding()) : CharsetUtil.UTF_8; encoders.add(ChannelHandlerFactories.newStringEncoder(charset, protocol)); ByteBuf[] delimiters = delimiter == TextLineDelimiter.LINE ? Delimiters.lineDelimiter() : Delimiters.nulDelimiter(); decoders.add(ChannelHandlerFactories.newDelimiterBasedFrameDecoder(decoderMaxLineLength, delimiters, protocol)); decoders.add(ChannelHandlerFactories.newStringDecoder(charset, protocol)); if (LOG.isDebugEnabled()) { LOG.debug("Using textline encoders and decoders with charset: {}, delimiter: {} and decoderMaxLineLength: {}", new Object[]{charset, delimiter, decoderMaxLineLength}); } } else if ("udp".equalsIgnoreCase(protocol) && isUdpByteArrayCodec()) { encoders.add(ChannelHandlerFactories.newByteArrayEncoder(protocol)); decoders.add(ChannelHandlerFactories.newByteArrayDecoder(protocol)); } else { // object serializable is then used encoders.add(ChannelHandlerFactories.newObjectEncoder(protocol)); decoders.add(ChannelHandlerFactories.newObjectDecoder(protocol)); LOG.debug("Using object encoders and decoders"); } if ("udp".equalsIgnoreCase(protocol)) { decoders.add(ChannelHandlerFactories.newDatagramPacketDecoder()); } } else { LOG.debug("No encoders and decoders will be used"); } } else { LOG.debug("Using configured encoders and/or decoders"); } } public String getCharsetName() { if (encoding == null) { return null; } if (!Charset.isSupported(encoding)) { throw new IllegalArgumentException("The encoding: " + encoding + " is not supported"); } return Charset.forName(encoding).name(); } public long getRequestTimeout() { return requestTimeout; } /** * Allows to use a timeout for the Netty producer when calling a remote server. * By default no timeout is in use. The value is in milli seconds, so eg 30000 is 30 seconds. * The requestTimeout is using Netty's ReadTimeoutHandler to trigger the timeout. */ public void setRequestTimeout(long requestTimeout) { this.requestTimeout = requestTimeout; } public boolean isSync() { return sync; } /** * Setting to set endpoint as one-way or request-response */ public void setSync(boolean sync) { this.sync = sync; } public boolean isTextline() { return textline; } /** * Only used for TCP. If no codec is specified, you can use this flag to indicate a text line based codec; * if not specified or the value is false, then Object Serialization is assumed over TCP. */ public void setTextline(boolean textline) { this.textline = textline; } public int getDecoderMaxLineLength() { return decoderMaxLineLength; } /** * The max line length to use for the textline codec. */ public void setDecoderMaxLineLength(int decoderMaxLineLength) { this.decoderMaxLineLength = decoderMaxLineLength; } public TextLineDelimiter getDelimiter() { return delimiter; } /** * The delimiter to use for the textline codec. Possible values are LINE and NULL. */ public void setDelimiter(TextLineDelimiter delimiter) { this.delimiter = delimiter; } public boolean isAutoAppendDelimiter() { return autoAppendDelimiter; } /** * Whether or not to auto append missing end delimiter when sending using the textline codec. */ public void setAutoAppendDelimiter(boolean autoAppendDelimiter) { this.autoAppendDelimiter = autoAppendDelimiter; } public String getEncoding() { return encoding; } /** * The encoding (a charset name) to use for the textline codec. If not provided, Camel will use the JVM default Charset. */ public void setEncoding(String encoding) { this.encoding = encoding; } public List<ChannelHandler> getDecoders() { return decoders; } /** * A list of decoders to be used. * You can use a String which have values separated by comma, and have the values be looked up in the Registry. * Just remember to prefix the value with # so Camel knows it should lookup. */ public void setDecoders(List<ChannelHandler> decoders) { this.decoders = decoders; } public List<ChannelHandler> getEncoders() { return encoders; } /** * A list of encoders to be used. You can use a String which have values separated by comma, and have the values be looked up in the Registry. * Just remember to prefix the value with # so Camel knows it should lookup. */ public void setEncoders(List<ChannelHandler> encoders) { this.encoders = encoders; } public ChannelHandler getEncoder() { return encoders.isEmpty() ? null : encoders.get(0); } /** * A custom ChannelHandler class that can be used to perform special marshalling of outbound payloads. */ public void setEncoder(ChannelHandler encoder) { if (!encoders.contains(encoder)) { encoders.add(encoder); } } public ChannelHandler getDecoder() { return decoders.isEmpty() ? null : decoders.get(0); } /** * A custom ChannelHandler class that can be used to perform special marshalling of inbound payloads. */ public void setDecoder(ChannelHandler decoder) { if (!decoders.contains(decoder)) { decoders.add(decoder); } } public boolean isDisconnect() { return disconnect; } /** * Whether or not to disconnect(close) from Netty Channel right after use. Can be used for both consumer and producer. */ public void setDisconnect(boolean disconnect) { this.disconnect = disconnect; } public boolean isLazyChannelCreation() { return lazyChannelCreation; } /** * Channels can be lazily created to avoid exceptions, if the remote server is not up and running when the Camel producer is started. */ public void setLazyChannelCreation(boolean lazyChannelCreation) { this.lazyChannelCreation = lazyChannelCreation; } public boolean isTransferExchange() { return transferExchange; } /** * Only used for TCP. You can transfer the exchange over the wire instead of just the body. * The following fields are transferred: In body, Out body, fault body, In headers, Out headers, fault headers, * exchange properties, exchange exception. * This requires that the objects are serializable. Camel will exclude any non-serializable objects and log it at WARN level. */ public void setTransferExchange(boolean transferExchange) { this.transferExchange = transferExchange; } public boolean isAllowSerializedHeaders() { return allowSerializedHeaders; } /** * Only used for TCP when transferExchange is true. When set to true, serializable objects in headers and properties * will be added to the exchange. Otherwise Camel will exclude any non-serializable objects and log it at WARN * level. */ public void setAllowSerializedHeaders(final boolean allowSerializedHeaders) { this.allowSerializedHeaders = allowSerializedHeaders; } public boolean isDisconnectOnNoReply() { return disconnectOnNoReply; } /** * If sync is enabled then this option dictates NettyConsumer if it should disconnect where there is no reply to send back. */ public void setDisconnectOnNoReply(boolean disconnectOnNoReply) { this.disconnectOnNoReply = disconnectOnNoReply; } public LoggingLevel getNoReplyLogLevel() { return noReplyLogLevel; } /** * If sync is enabled this option dictates NettyConsumer which logging level to use when logging a there is no reply to send back. */ public void setNoReplyLogLevel(LoggingLevel noReplyLogLevel) { this.noReplyLogLevel = noReplyLogLevel; } public LoggingLevel getServerExceptionCaughtLogLevel() { return serverExceptionCaughtLogLevel; } /** * If the server (NettyConsumer) catches an exception then its logged using this logging level. */ public void setServerExceptionCaughtLogLevel(LoggingLevel serverExceptionCaughtLogLevel) { this.serverExceptionCaughtLogLevel = serverExceptionCaughtLogLevel; } public LoggingLevel getServerClosedChannelExceptionCaughtLogLevel() { return serverClosedChannelExceptionCaughtLogLevel; } /** * If the server (NettyConsumer) catches an java.nio.channels.ClosedChannelException then its logged using this logging level. * This is used to avoid logging the closed channel exceptions, as clients can disconnect abruptly and then cause a flood of closed exceptions in the Netty server. */ public void setServerClosedChannelExceptionCaughtLogLevel(LoggingLevel serverClosedChannelExceptionCaughtLogLevel) { this.serverClosedChannelExceptionCaughtLogLevel = serverClosedChannelExceptionCaughtLogLevel; } public boolean isAllowDefaultCodec() { return allowDefaultCodec; } /** * The netty component installs a default codec if both, encoder/deocder is null and textline is false. * Setting allowDefaultCodec to false prevents the netty component from installing a default codec as the first element in the filter chain. */ public void setAllowDefaultCodec(boolean allowDefaultCodec) { this.allowDefaultCodec = allowDefaultCodec; } /** * @deprecated use #setClientInitializerFactory */ @Deprecated public void setClientPipelineFactory(ClientInitializerFactory clientPipelineFactory) { this.clientInitializerFactory = clientPipelineFactory; } /** * @deprecated use #getClientInitializerFactory */ @Deprecated public ClientInitializerFactory getClientPipelineFactory() { return clientInitializerFactory; } public ClientInitializerFactory getClientInitializerFactory() { return clientInitializerFactory; } /** * To use a custom ClientInitializerFactory */ public void setClientInitializerFactory(ClientInitializerFactory clientInitializerFactory) { this.clientInitializerFactory = clientInitializerFactory; } public boolean isUsingExecutorService() { return usingExecutorService; } /** * Whether to use ordered thread pool, to ensure events are processed orderly on the same channel. */ public void setUsingExecutorService(boolean usingExecutorService) { this.usingExecutorService = usingExecutorService; } public int getProducerPoolMaxActive() { return producerPoolMaxActive; } /** * Sets the cap on the number of objects that can be allocated by the pool * (checked out to clients, or idle awaiting checkout) at a given time. Use a negative value for no limit. */ public void setProducerPoolMaxActive(int producerPoolMaxActive) { this.producerPoolMaxActive = producerPoolMaxActive; } public int getProducerPoolMinIdle() { return producerPoolMinIdle; } /** * Sets the minimum number of instances allowed in the producer pool before the evictor thread (if active) spawns new objects. */ public void setProducerPoolMinIdle(int producerPoolMinIdle) { this.producerPoolMinIdle = producerPoolMinIdle; } public int getProducerPoolMaxIdle() { return producerPoolMaxIdle; } /** * Sets the cap on the number of "idle" instances in the pool. */ public void setProducerPoolMaxIdle(int producerPoolMaxIdle) { this.producerPoolMaxIdle = producerPoolMaxIdle; } public long getProducerPoolMinEvictableIdle() { return producerPoolMinEvictableIdle; } /** * Sets the minimum amount of time (value in millis) an object may sit idle in the pool before it is eligible for eviction by the idle object evictor. */ public void setProducerPoolMinEvictableIdle(long producerPoolMinEvictableIdle) { this.producerPoolMinEvictableIdle = producerPoolMinEvictableIdle; } public boolean isProducerPoolEnabled() { return producerPoolEnabled; } /** * Whether producer pool is enabled or not. * Important: Do not turn this off, as the pooling is needed for handling concurrency and reliable request/reply. */ public void setProducerPoolEnabled(boolean producerPoolEnabled) { this.producerPoolEnabled = producerPoolEnabled; } public boolean isUdpConnectionlessSending() { return udpConnectionlessSending; } /** * This option supports connection less udp sending which is a real fire and forget. * A connected udp send receive the PortUnreachableException if no one is listen on the receiving port. */ public void setUdpConnectionlessSending(boolean udpConnectionlessSending) { this.udpConnectionlessSending = udpConnectionlessSending; } public boolean isClientMode() { return clientMode; } /** * If the clientMode is true, netty consumer will connect the address as a TCP client. */ public void setClientMode(boolean clientMode) { this.clientMode = clientMode; } public boolean isUseByteBuf() { return useByteBuf; } /** * If the useByteBuf is true, netty producer will turn the message body into {@link ByteBuf} before sending it out. */ public void setUseByteBuf(boolean useByteBuf) { this.useByteBuf = useByteBuf; } public boolean isUdpByteArrayCodec() { return udpByteArrayCodec; } /** * For UDP only. If enabled the using byte array codec instead of Java serialization protocol. */ public void setUdpByteArrayCodec(boolean udpByteArrayCodec) { this.udpByteArrayCodec = udpByteArrayCodec; } public boolean isReuseChannel() { return reuseChannel; } /** * This option allows producers to reuse the same Netty {@link Channel} for the lifecycle of processing the {@link Exchange}. * This is useable if you need to call a server multiple times in a Camel route and want to use the same network connection. * When using this the channel is not returned to the connection pool until the {@link Exchange} is done; or disconnected * if the disconnect option is set to true. * <p/> * The reused {@link Channel} is stored on the {@link Exchange} as an exchange property with the key {@link NettyConstants#NETTY_CHANNEL} * which allows you to obtain the channel during routing and use it as well. */ public void setReuseChannel(boolean reuseChannel) { this.reuseChannel = reuseChannel; } private static <T> void addToHandlersList(List<T> configured, List<T> handlers, Class<T> handlerType) { if (handlers != null) { for (T handler : handlers) { if (handlerType.isInstance(handler)) { configured.add(handler); } } } } }