/*
* 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.tcp.connection;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.mapping.InboundMessageMapper;
import org.springframework.integration.mapping.OutboundMessageMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
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.InvalidMimeTypeException;
import org.springframework.util.MimeType;
/**
* Maps incoming data from a {@link TcpConnection} to a {@link Message}.
* If StringToBytes is true (default),
* payloads of type String are converted to a byte[] using the supplied
* charset (UTF-8 by default).
* Inbound messages include headers representing the remote end of the
* connection as well as a connection id that can be used by a {@link TcpSender}
* to correlate which connection to send a reply. If applySequence is set, adds
* standard correlationId/sequenceNumber headers allowing for downstream (unbounded)
* resequencing.
* *
* @author Gary Russell
* @author Artem Bilan
* @since 2.0
*
*/
public class TcpMessageMapper implements
InboundMessageMapper<TcpConnection>,
OutboundMessageMapper<Object>,
BeanFactoryAware {
protected final Log logger = LogFactory.getLog(this.getClass());
private volatile String charset = "UTF-8";
private volatile boolean stringToBytes = true;
private volatile boolean applySequence = false;
private volatile MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
private volatile boolean messageBuilderFactorySet;
private volatile String contentType = "application/octet-stream;charset=" + this.charset;
private volatile boolean addContentTypeHeader;
private BeanFactory beanFactory;
/**
* Set the charset to use when converting outbound String messages to {@code byte[]}.
* @param charset the charset to set
*/
public void setCharset(String charset) {
this.charset = charset;
}
/**
* Sets whether outbound String payloads are to be converted
* to byte[]. Default is true.
* @param stringToBytes The stringToBytes to set.
*/
public void setStringToBytes(boolean stringToBytes) {
this.stringToBytes = stringToBytes;
}
/**
* @param applySequence The applySequence to set.
*/
public void setApplySequence(boolean applySequence) {
this.applySequence = applySequence;
}
/**
* Set the content type header value to add to inbound messages when
* {@link #setAddContentTypeHeader(boolean) addContentTypeHeader} is true.
* Default {@code application/octet-stream;charset=UTF-8}. This default is <b>not</b>
* modified by {@link #setCharset(String)}.
* @param contentType the content type header value to set.
* @since 4.3
* @see #setAddContentTypeHeader(boolean)
* @see TcpMessageMapper#setCharset(String)
*/
public void setContentType(String contentType) {
Assert.notNull(contentType, "'contentType' cannot be null");
try {
MimeType.valueOf(contentType);
}
catch (InvalidMimeTypeException e) {
throw new IllegalArgumentException("'contentType' could not be parsed", e);
}
this.contentType = contentType;
}
/**
* Set to true to add a content type header; default false.
* @param addContentTypeHeader true to add a content type header.
* @since 4.3
* @see #setContentType(String)
*/
public void setAddContentTypeHeader(boolean addContentTypeHeader) {
this.addContentTypeHeader = addContentTypeHeader;
}
@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;
}
@Override
public Message<?> toMessage(TcpConnection connection) throws Exception {
Message<Object> message = null;
Object payload = connection.getPayload();
if (payload != null) {
AbstractIntegrationMessageBuilder<Object> messageBuilder = getMessageBuilderFactory().withPayload(payload);
this.addStandardHeaders(connection, messageBuilder);
this.addCustomHeaders(connection, messageBuilder);
message = messageBuilder.build();
}
else {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Null payload from connection " + connection.getConnectionId());
}
}
return message;
}
protected final void addStandardHeaders(TcpConnection connection,
AbstractIntegrationMessageBuilder<?> messageBuilder) {
String connectionId = connection.getConnectionId();
messageBuilder
.setHeader(IpHeaders.HOSTNAME, connection.getHostName())
.setHeader(IpHeaders.IP_ADDRESS, connection.getHostAddress())
.setHeader(IpHeaders.REMOTE_PORT, connection.getPort())
.setHeader(IpHeaders.CONNECTION_ID, connectionId);
SocketInfo socketInfo = connection.getSocketInfo();
if (socketInfo != null) {
messageBuilder.setHeader(IpHeaders.LOCAL_ADDRESS, socketInfo.getLocalAddress());
}
if (this.applySequence) {
messageBuilder
.setCorrelationId(connectionId)
.setSequenceNumber((int) connection.incrementAndGetConnectionSequence());
}
if (this.addContentTypeHeader) {
messageBuilder.setHeader(MessageHeaders.CONTENT_TYPE, this.contentType);
}
}
protected final void addCustomHeaders(TcpConnection connection,
AbstractIntegrationMessageBuilder<?> messageBuilder) {
Map<String, ?> customHeaders = this.supplyCustomHeaders(connection);
if (customHeaders != null) {
messageBuilder.copyHeadersIfAbsent(customHeaders);
}
}
/**
* Override to provide additional headers. The standard headers cannot be overridden
* and any such headers will be ignored if provided in the result.
* @param connection the connection.
* @return A Map of {@code <String, ?>} headers to be added to the message.
*/
protected Map<String, ?> supplyCustomHeaders(TcpConnection connection) {
return null;
}
@Override
public Object fromMessage(Message<?> message) throws Exception {
if (this.stringToBytes) {
return getPayloadAsBytes(message);
}
return message.getPayload();
}
/**
* Extracts the payload as a byte array.
*/
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,
"When using a byte array serializer, the socket mapper expects " +
"either a byte array or String payload, but received: " + payload.getClass());
}
return bytes;
}
}