/*
* Copyright 2002-2016 the original author or authors.
*
* 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 org.springframework.integration.ip.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.integration.ip.AbstractInternetProtocolReceivingChannelAdapter;
import org.springframework.integration.ip.IpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
/**
* A channel adapter to receive incoming UDP packets. Packets can optionally be preceded by a
* 4 byte length field, used to validate that all data was received. Packets may also contain
* information indicating an acknowledgment needs to be sent.
*
* @author Gary Russell
* @since 2.0
*/
public class UnicastReceivingChannelAdapter extends AbstractInternetProtocolReceivingChannelAdapter {
private volatile DatagramSocket socket;
private final DatagramPacketMessageMapper mapper = new DatagramPacketMessageMapper();
private volatile int soSendBufferSize = -1;
private static Pattern addressPattern = Pattern.compile("([^:]*):([0-9]*)");
/**
* Constructs a UnicastReceivingChannelAdapter that listens on the specified port.
* @param port The port.
*/
public UnicastReceivingChannelAdapter(int port) {
super(port);
this.mapper.setLengthCheck(false);
}
/**
* Constructs a UnicastReceivingChannelAdapter that listens for packets on
* the specified port. Enables setting the lengthCheck option, which expects
* a length to precede the incoming packets.
* @param port The port.
* @param lengthCheck If true, enables the lengthCheck Option.
*/
public UnicastReceivingChannelAdapter(int port, boolean lengthCheck) {
super(port);
this.mapper.setLengthCheck(lengthCheck);
}
/**
* @param lengthCheck if true, the incoming packet is expected to have a four
* byte binary length header.
* @since 5.0
*/
public void setLengthCheck(boolean lengthCheck) {
this.mapper.setLengthCheck(lengthCheck);
}
@Override
public boolean isLongLived() {
return true;
}
@Override
public int getPort() {
if (this.socket == null) {
return super.getPort();
}
else {
return this.socket.getLocalPort();
}
}
@Override
protected void onInit() {
super.onInit();
this.mapper.setBeanFactory(this.getBeanFactory());
}
@Override
public void run() {
getSocket();
if (logger.isDebugEnabled()) {
logger.debug("UDP Receiver running on port:" + this.getPort());
}
setListening(true);
// Do as little as possible here so we can loop around and catch the next packet.
// Just schedule the packet for processing.
while (this.isActive()) {
try {
asyncSendMessage(receive());
}
catch (SocketTimeoutException e) {
// continue
}
catch (SocketException e) {
this.stop();
}
catch (Exception e) {
if (e instanceof MessagingException) {
throw (MessagingException) e;
}
throw new MessagingException("failed to receive DatagramPacket", e);
}
}
this.setListening(false);
}
protected void sendAck(Message<byte[]> message) {
MessageHeaders headers = message.getHeaders();
Object id = headers.get(IpHeaders.ACK_ID);
byte[] ack = id.toString().getBytes();
String ackAddress = ((String) headers.get(IpHeaders.ACK_ADDRESS)).trim();
Matcher mat = addressPattern.matcher(ackAddress);
if (!mat.matches()) {
throw new MessagingException(message,
"Ack requested but could not decode acknowledgment address: " + ackAddress);
}
String host = mat.group(1);
int port = Integer.parseInt(mat.group(2));
InetSocketAddress whereTo = new InetSocketAddress(host, port);
if (logger.isDebugEnabled()) {
logger.debug("Sending ack for " + id + " to " + ackAddress);
}
try {
DatagramPacket ackPack = new DatagramPacket(ack, ack.length, whereTo);
DatagramSocket out = new DatagramSocket();
if (this.soSendBufferSize > 0) {
out.setSendBufferSize(this.soSendBufferSize);
}
out.send(ackPack);
out.close();
}
catch (IOException e) {
throw new MessagingException(message, "Failed to send acknowledgment to: " + ackAddress, e);
}
}
protected boolean asyncSendMessage(final DatagramPacket packet) {
Executor taskExecutor = getTaskExecutor();
if (taskExecutor != null) {
try {
taskExecutor.execute(() -> doSend(packet));
}
catch (RejectedExecutionException e) {
if (logger.isDebugEnabled()) {
logger.debug("Adapter stopped, sending on main thread");
}
doSend(packet);
}
}
return true;
}
protected void doSend(final DatagramPacket packet) {
Message<byte[]> message = null;
try {
message = this.mapper.toMessage(packet);
if (logger.isDebugEnabled()) {
logger.debug("Received:" + message);
}
}
catch (Exception e) {
logger.error("Failed to map packet to message ", e);
}
if (message != null) {
if (message.getHeaders().containsKey(IpHeaders.ACK_ADDRESS)) {
sendAck(message);
}
sendMessage(message);
}
}
protected DatagramPacket receive() throws Exception {
DatagramSocket socket = this.getSocket();
final byte[] buffer = new byte[this.getReceiveBufferSize()];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
return packet;
}
/**
* @param socket the socket to set
*/
public void setSocket(DatagramSocket socket) {
this.socket = socket;
}
protected DatagramSocket getTheSocket() {
return this.socket;
}
public synchronized DatagramSocket getSocket() {
if (this.socket == null) {
try {
DatagramSocket socket = null;
String localAddress = this.getLocalAddress();
int port = super.getPort();
if (localAddress == null) {
socket = port == 0 ? new DatagramSocket() : new DatagramSocket(port);
}
else {
InetAddress whichNic = InetAddress.getByName(localAddress);
socket = new DatagramSocket(new InetSocketAddress(whichNic, port));
}
setSocketAttributes(socket);
this.socket = socket;
}
catch (IOException e) {
throw new MessagingException("failed to create DatagramSocket", e);
}
}
return this.socket;
}
/**
* Sets timeout and receive buffer size
*
* @param socket The socket.
* @throws SocketException Any socket exception.
*/
protected void setSocketAttributes(DatagramSocket socket)
throws SocketException {
socket.setSoTimeout(this.getSoTimeout());
int soReceiveBufferSize = this.getSoReceiveBufferSize();
if (soReceiveBufferSize > 0) {
socket.setReceiveBufferSize(soReceiveBufferSize);
}
}
@Override
protected void doStop() {
super.doStop();
try {
DatagramSocket socket = this.socket;
this.socket = null;
socket.close();
}
catch (Exception e) {
// ignore
}
}
@Override
public void setSoSendBufferSize(int soSendBufferSize) {
this.soSendBufferSize = soSendBufferSize;
}
public void setLookupHost(boolean lookupHost) {
this.mapper.setLookupHost(lookupHost);
}
@Override
public String getComponentType() {
return "ip:udp-inbound-channel-adapter";
}
}