org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload Maven / Gradle / Ivy
/*
* Copyright 2012-2015 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.boot.devtools.tunnel.payload;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Encapsulates a payload data sent via a HTTP tunnel.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpTunnelPayload {
private static final String SEQ_HEADER = "x-seq";
private static final int BUFFER_SIZE = 1024 * 100;
final protected static char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
private static final Log logger = LogFactory.getLog(HttpTunnelPayload.class);
private final long sequence;
private final ByteBuffer data;
/**
* Create a new {@link HttpTunnelPayload} instance.
* @param sequence the sequence number of the payload
* @param data the payload data
*/
public HttpTunnelPayload(long sequence, ByteBuffer data) {
Assert.isTrue(sequence > 0, "Sequence must be positive");
Assert.notNull(data, "Data must not be null");
this.sequence = sequence;
this.data = data;
}
/**
* Return the sequence number of the payload.
* @return the sequence
*/
public long getSequence() {
return this.sequence;
}
/**
* Assign this payload to the given {@link HttpOutputMessage}.
* @param message the message to assign this payload to
* @throws IOException in case of I/O errors
*/
public void assignTo(HttpOutputMessage message) throws IOException {
Assert.notNull(message, "Message must not be null");
HttpHeaders headers = message.getHeaders();
headers.setContentLength(this.data.remaining());
headers.add(SEQ_HEADER, Long.toString(getSequence()));
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
WritableByteChannel body = Channels.newChannel(message.getBody());
while (this.data.hasRemaining()) {
body.write(this.data);
}
body.close();
}
/**
* Write the content of this payload to the given target channel.
* @param channel the channel to write to
* @throws IOException in case of I/O errors
*/
public void writeTo(WritableByteChannel channel) throws IOException {
Assert.notNull(channel, "Channel must not be null");
while (this.data.hasRemaining()) {
channel.write(this.data);
}
}
/**
* Return the {@link HttpTunnelPayload} for the given message or {@code null} if there
* is no payload.
* @param message the HTTP message
* @return the payload or {@code null}
* @throws IOException in case of I/O errors
*/
public static HttpTunnelPayload get(HttpInputMessage message) throws IOException {
long length = message.getHeaders().getContentLength();
if (length <= 0) {
return null;
}
String seqHeader = message.getHeaders().getFirst(SEQ_HEADER);
Assert.state(StringUtils.hasLength(seqHeader), "Missing sequence header");
ReadableByteChannel body = Channels.newChannel(message.getBody());
ByteBuffer payload = ByteBuffer.allocate((int) length);
while (payload.hasRemaining()) {
body.read(payload);
}
body.close();
payload.flip();
return new HttpTunnelPayload(Long.valueOf(seqHeader), payload);
}
/**
* Return the payload data for the given source {@link ReadableByteChannel} or null if
* the channel timed out whilst reading.
* @param channel the source channel
* @return payload data or {@code null}
* @throws IOException in case of I/O errors
*/
public static ByteBuffer getPayloadData(ReadableByteChannel channel)
throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
try {
int amountRead = channel.read(buffer);
Assert.state(amountRead != -1, "Target server connection closed");
buffer.flip();
return buffer;
}
catch (InterruptedIOException ex) {
return null;
}
}
/**
* Log incoming payload information at trace level to aid diagnostics.
*/
public void logIncoming() {
log("< ");
}
/**
* Log incoming payload information at trace level to aid diagnostics.
*/
public void logOutgoing() {
log("> ");
}
private void log(String prefix) {
if (logger.isTraceEnabled()) {
logger.trace(prefix + toHexString());
}
}
/**
* Return the payload as a hexadecimal string.
* @return the payload as a hex string
*/
public String toHexString() {
byte[] bytes = this.data.array();
char[] hex = new char[this.data.remaining() * 2];
for (int i = this.data.position(); i < this.data.remaining(); i++) {
int b = bytes[i] & 0xFF;
hex[i * 2] = HEX_CHARS[b >>> 4];
hex[i * 2 + 1] = HEX_CHARS[b & 0x0F];
}
return new String(hex);
}
}