/*
* Copyright 2002-2017 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.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.nio.ByteBuffer;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.integration.ip.IpHeaders;
import org.springframework.integration.ip.util.RegexUtils;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.mapping.MessageMappingException;
import org.springframework.integration.mapping.OutboundMessageMapper;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Message Mapper for converting to and from UDP DatagramPackets. When
* converting to a Message, the payload will be a byte array containing the
* data from the received packet. When converting from a Message, the payload
* may be either a byte array or a String. The default charset for converting
* a String to a byte array is UTF-8, but that may be changed by invoking the
* {@link #setCharset(String)} method.
*
* By default, the UDP messages will be unreliable (truncation may occur on
* the receiving end; packets may be lost).
*
* Reliability can be enhanced by one or both of the following techniques:
* <ul>
* <li>including a binary message length at the beginning of the packet</li>
* <li>requesting a receipt acknowledgment</li>
* </ul>
*
* @author Mark Fisher
* @author Gary Russell
* @author Dave Syer
* @author Artem Bilan
* @since 2.0
*/
public class DatagramPacketMessageMapper implements InboundMessageMapper<DatagramPacket>,
OutboundMessageMapper<DatagramPacket>, BeanFactoryAware {
private volatile String charset = "UTF-8";
private boolean acknowledge = false;
private String ackAddress;
private boolean lengthCheck = false;
private boolean lookupHost = true;
private volatile MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
private volatile boolean messageBuilderFactorySet;
private static Pattern udpHeadersPattern =
Pattern.compile(RegexUtils.escapeRegexSpecials(IpHeaders.ACK_ADDRESS) +
"=" + "([^;]*);" +
RegexUtils.escapeRegexSpecials(MessageHeaders.ID) +
"=" + "([^;]*);");
private BeanFactory beanFactory;
public void setCharset(String charset) {
this.charset = charset;
}
public void setAcknowledge(boolean acknowledge) {
this.acknowledge = acknowledge;
}
public void setAckAddress(String ackAddress) {
this.ackAddress = ackAddress;
}
public void setLengthCheck(boolean lengthCheck) {
this.lengthCheck = lengthCheck;
}
/**
* @param lookupHost the lookupHost to set
*/
public void setLookupHost(boolean lookupHost) {
this.lookupHost = lookupHost;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
protected MessageBuilderFactory getMessageBuilderFactory() {
if (!this.messageBuilderFactorySet) {
if (this.beanFactory != null) {
this.messageBuilderFactory = IntegrationUtils.getMessageBuilderFactory(this.beanFactory);
}
this.messageBuilderFactorySet = true;
}
return this.messageBuilderFactory;
}
/**
* Raw byte[] from message, possibly with a length field up front.
*/
@Override
public DatagramPacket fromMessage(Message<?> message) throws Exception {
if (this.acknowledge) {
return fromMessageWithAck(message);
}
byte[] bytes = getPayloadAsBytes(message);
if (this.lengthCheck) {
ByteBuffer buffer = ByteBuffer.allocate(bytes.length + 4);
// insert the length (not including the length bytes)
// default ByteOrder is ByteOrder.BIG_ENDIAN (network byte order)
buffer.putInt(bytes.length);
buffer.put(bytes);
bytes = buffer.array();
}
return new DatagramPacket(bytes, bytes.length);
}
/**
* Prefix raw byte[] from message with 'acknowledge to' and 'message id' "headers".
*/
private DatagramPacket fromMessageWithAck(Message<?> message) throws Exception {
Assert.state(StringUtils.hasText(this.ackAddress), "'ackAddress' must not be empty");
byte[] bytes = getPayloadAsBytes(message);
ByteBuffer buffer = ByteBuffer.allocate(100 + bytes.length);
if (this.lengthCheck) {
buffer.putInt(0); // placeholder for length
}
buffer.put(IpHeaders.ACK_ADDRESS.getBytes(this.charset));
buffer.put((byte) '=');
buffer.put(this.ackAddress.getBytes(this.charset));
buffer.put((byte) ';');
buffer.put(MessageHeaders.ID.getBytes(this.charset));
buffer.put((byte) '=');
buffer.put(message.getHeaders().getId().toString().getBytes(this.charset));
buffer.put((byte) ';');
int headersLength = buffer.position() - 4;
buffer.put(bytes);
if (this.lengthCheck) {
// insert the length (not including the length bytes)
// default ByteOrder is ByteOrder.BIG_ENDIAN (network byte order)
buffer.putInt(0, bytes.length + headersLength);
}
return new DatagramPacket(buffer.array(), buffer.position());
}
private byte[] getPayloadAsBytes(Message<?> message) {
byte[] bytes = null;
Object payload = message.getPayload();
if (payload instanceof byte[]) {
bytes = (byte[]) payload;
}
else if (payload instanceof String) {
try {
bytes = ((String) payload).getBytes(this.charset);
}
catch (UnsupportedEncodingException e) {
throw new MessageHandlingException(message, e);
}
}
else {
throw new MessageHandlingException(message, "The datagram packet mapper expects " +
"either a byte array or String payload, but received: " + payload.getClass());
}
return bytes;
}
@Override
public Message<byte[]> toMessage(DatagramPacket packet) throws Exception {
int offset = packet.getOffset();
int length = packet.getLength();
byte[] payload;
ByteBuffer buffer = ByteBuffer.wrap(packet.getData(), offset, length);
Message<byte[]> message = null;
if (this.lengthCheck) {
int declaredLength = buffer.getInt();
if (declaredLength != (length - 4)) {
throw new MessageMappingException("Incorrect length; expected " + (declaredLength + 4) + ", received " + length);
}
offset += 4;
length -= 4;
}
String hostAddress = packet.getAddress().getHostAddress();
String hostName;
if (this.lookupHost) {
hostName = packet.getAddress().getHostName();
}
else {
hostName = hostAddress;
}
int port = packet.getPort();
// Peek at the message in case they didn't configure us for ack but the sending
// side expects it.
if (this.acknowledge || startsWith(buffer, IpHeaders.ACK_ADDRESS)) {
try {
String headers = new String(packet.getData(), offset, length, this.charset);
Matcher matcher = udpHeadersPattern.matcher(headers);
if (matcher.find()) {
// Strip off the ack headers and put in Message headers
length = length - matcher.end();
payload = new byte[length];
System.arraycopy(packet.getData(), offset + matcher.end(), payload, 0, length);
message = getMessageBuilderFactory().withPayload(payload)
.setHeader(IpHeaders.ACK_ID, UUID.fromString(matcher.group(2)))
.setHeader(IpHeaders.ACK_ADDRESS, matcher.group(1))
.setHeader(IpHeaders.HOSTNAME, hostName)
.setHeader(IpHeaders.IP_ADDRESS, hostAddress)
.setHeader(IpHeaders.PORT, port)
.setHeader(IpHeaders.PACKET_ADDRESS, packet.getSocketAddress())
.build();
} // on no match, just treat as simple payload
}
catch (UnsupportedEncodingException e) {
throw new MessageMappingException("Invalid charset", e);
}
}
if (message == null) {
payload = new byte[length];
System.arraycopy(packet.getData(), offset, payload, 0, length);
if (payload.length > 0) {
message = getMessageBuilderFactory().withPayload(payload)
.setHeader(IpHeaders.HOSTNAME, hostName)
.setHeader(IpHeaders.IP_ADDRESS, hostAddress)
.setHeader(IpHeaders.PORT, port)
.setHeader(IpHeaders.PACKET_ADDRESS, packet.getSocketAddress())
.build();
}
}
return message;
}
/**
* Peeks at data in the buffer to see if starts with the prefix.
*/
private boolean startsWith(ByteBuffer buffer, String prefix) {
int pos = buffer.position();
if (buffer.limit() - pos < prefix.length()) {
return false;
}
try {
byte[] comparing;
comparing = prefix.getBytes(this.charset);
for (int i = 0; i < comparing.length; i++) {
if (buffer.get() != comparing[i]) {
return false;
}
}
return true;
}
catch (UnsupportedEncodingException e) {
throw new MessageMappingException("Invalid charset", e);
}
finally {
//reposition the buffer
buffer.position(pos);
}
}
}