/**
* Copyright 2012 Terremark Worldwide Inc.
*
* 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 com.terremark.handlers;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.wink.client.ClientRequest;
import org.apache.wink.client.ClientResponse;
import org.apache.wink.client.handlers.ClientHandler;
import org.apache.wink.client.handlers.HandlerContext;
import org.apache.wink.client.handlers.InputStreamAdapter;
import org.apache.wink.client.handlers.OutputStreamAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Logging handler. Logger {@code com.terremark.data} is active only when category is set to {@code DEBUG} level. Setting logger to debug
* level will log the HTTP headers and data (if any).
*
* @author <a href="mailto:spasam@terremark.com">Seshu Pasam</a>
*/
public class HTTPLoggingHandler implements ClientHandler
{
/** Logger */
private static final Logger LOGGER = LoggerFactory.getLogger("com.terremark.data");
/** Platform specified new line */
private static final String NL = System.getProperty("line.separator");
/**
* Method invoked in the chain. The request is logged, if necessary. The chain is invoked. Once the response is received, it is logged.
*
* @param request Client request.
* @param context Request context.
* @return Client response.
* @throws Exception If there is a problem invoking the chain.
*/
@Override
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
public final ClientResponse handle(final ClientRequest request, final HandlerContext context) throws Exception
{
if (LOGGER.isDebugEnabled()) {
if (request.getEntity() == null) {
// Log headers here since the adapter for output stream will not be called without an entity
StringBuilder builder = new StringBuilder().append("Request").append(NL).append(request.getMethod()).append(' ')
.append(request.getURI().toString()).append(NL);
for (final Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
if (entry.getKey() != null) {
builder.append(entry.getKey()).append(": ").append(entry.getValue()).append(NL);
}
}
LOGGER.debug("{}", builder);
} else {
// Add adapters for output stream
context.addOutputStreamAdapter(new OutputStreamAdapter() {
@Override
public OutputStream adapt(final OutputStream os, final ClientRequest request) throws IOException
{
return new LoggingOutputStream(os, request.getMethod(), request.getURI(), request.getHeaders());
}
});
}
// Add adapters for input stream
context.addInputStreamAdapter(new InputStreamAdapter() {
@Override
public InputStream adapt(final InputStream is, final ClientResponse response) throws IOException
{
return new LoggingInputStream(is, response.getStatusCode(), response.getStatusType(), response.getHeaders());
}
});
}
// Invoke the chain
return context.doChain(request);
}
/**
* Output stream delegate wrapper.
*
* @author <a href="mailto:spasam@terremark.com">Seshu Pasam</a>
*/
static class LoggingOutputStream extends OutputStream
{
/** Actual output stream */
private final OutputStream os;
/** String builder to aggregate data */
private final StringBuilder builder;
/**
* Default constructor.
*
* @param os Actual output stream.
* @param method Request method.
* @param uri Request URI.
* @param headers Request headers.
*/
LoggingOutputStream(final OutputStream os, String method, URI uri, MultivaluedMap<String, String> headers)
{
this.os = os;
this.builder = new StringBuilder().append("Request").append(NL).append(method).append(' ').append(uri.toString()).append(NL);
for (final Map.Entry<String, List<String>> entry : headers.entrySet()) {
if (entry.getKey() != null) {
this.builder.append(entry.getKey()).append(": ").append(entry.getValue()).append(NL);
}
}
this.builder.append(NL);
}
/**
* If there is data, log it and close the output stream.
*
* @see java.io.OutputStream#close()
*/
@Override
public void close() throws IOException
{
LOGGER.debug("{}{}", builder, NL);
super.close();
}
/**
* Writes the specified byte to the actual output stream. The data is appended to the string aggregator. If the byte is a new line
* or line feed, it is logged.
*
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(final int i) throws IOException
{
os.write(i);
builder.append((char) i);
}
}
/**
* Input stream delegate wrapper.
*
* @author <a href="mailto:spasam@terremark.com">Seshu Pasam</a>
*/
static class LoggingInputStream extends InputStream
{
/** Actual input stream */
private final InputStream is;
/** String builder to aggregate data */
private final StringBuilder builder;
/**
* Default constructor.
*
* @param code Response code.
* @param status Response status type.
* @param headers Response headers.
* @param is Actual input stream.
*/
LoggingInputStream(final InputStream is, final int code, final Response.StatusType status, MultivaluedMap<String, String> headers)
{
this.is = is;
this.builder = new StringBuilder().append("Response").append(NL).append(code).append(' ').append(status.toString()).append(NL);
for (final Map.Entry<String, List<String>> entry : headers.entrySet()) {
if (entry.getKey() != null) {
this.builder.append(entry.getKey()).append(": ").append(entry.getValue()).append(NL);
}
}
this.builder.append(NL);
}
/**
* If there is data, log it and close the input stream.
*
* @see java.io.InputStream#close()
*/
@Override
public void close() throws IOException
{
LOGGER.debug("{}{}", builder, NL);
super.close();
}
/**
* Reads one byte from the actual input stream. The data is appended to the string aggregator. If EOF is reached or if the byte read
* is a new line or line feed, it is logged.
*
* @see java.io.InputStream#read()
*/
@Override
public int read() throws IOException
{
final int i = is.read();
if (i > 0) {
builder.append((char) i);
}
return i;
}
}
}