/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.ros.internal.xmlrpc.webserver;
import static org.jboss.netty.handler.codec.http.HttpHeaders.getContentLength;
import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import com.google.common.collect.Maps;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.common.ServerStreamConnection;
import org.apache.xmlrpc.server.XmlRpcHttpServerConfig;
import org.apache.xmlrpc.server.XmlRpcStreamServer;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.ros.exception.RosRuntimeException;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
/**
* A connection between an XMLRPC server and a client for that server.
*
* <p>
* One of these is created for each request that comes in.
*
* <p>
* This code is derived from the web server which came with the Apache XMLRPC server.
*
* @author Apache
* @author Keith M. Hughes
*/
public class XmlRpcServerClientConnection implements ServerStreamConnection {
/**
* The channel buffer for writing content.
*/
private ChannelBuffer channelBuffer;
/**
* The output stream for writing the XMLRPC response.
*/
private ChannelBufferOutputStream outputStream;
/**
* The Netty handler context.
*/
private ChannelHandlerContext ctx;
/**
* The Apache server for handling RPC calls.
*/
private XmlRpcStreamServer xmlRpcServer;
/**
* Data for the request.
*/
private XmlRpcServerClientRequestData requestData;
/**
* The HTTP request for the RPC call.
*/
private HttpRequest request;
/**
* Input stream from the HTTP request for the RPC call.
*/
private InputStream inputStream;
/**
* Netty Web Server handler that processes the HTTP requests.
*/
private NettyXmlRpcWebServerHandler handler;
/**
* HTTP headers for the response.
*/
private Map<String, String> headers = Maps.newHashMap();
/**
* If not {@code null} at the end of processing, something bad happened during processing.
*/
private Throwable processingThrowable;
/**
* Construct an XML RPC server client connection.
*
* @param ctx
* the Netty context
* @param request
* the HTTP request
* @param xmlRpcServer
* the server which will handle the requests
* @param handler
* the handler for requests
*/
public XmlRpcServerClientConnection(ChannelHandlerContext ctx, HttpRequest request, XmlRpcStreamServer xmlRpcServer,
NettyXmlRpcWebServerHandler handler) {
this.ctx = ctx;
this.request = request;
this.xmlRpcServer = xmlRpcServer;
this.handler = handler;
channelBuffer = ChannelBuffers.dynamicBuffer();
outputStream = new ChannelBufferOutputStream(channelBuffer);
}
/**
* Process the XMLRPC request from the connection.
*/
public void process() {
XmlRpcServerClientRequestData data = getRequestConfig();
try {
xmlRpcServer.execute(data, this);
// TODO(keith): At the moment not paying attention to
// processingThrowable since it looks like for the most part the
// responses to be sent back are not important. They will be logged
// and should be examined over time. The original apache code had
// special return headers for BadRequestException,
// BadEncodingException, and XmlRpcNotAuthorizedException, none of
// which are needed for ROS. Otherwise errors return a 200.
DefaultHttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
// for (Entry<String, String> header : headers.entrySet()) {
// res.addHeader(header.getKey(), header.getValue());
// }
res.setContent(getChannelBuffer());
handler.sendHttpResponse(ctx, request, res);
} catch (XmlRpcException e) {
handler.getWebServer().getLog().error("Error during XMLRPC server request handling", e);
}
}
/**
* Get the connections request configuration by merging the HTTP request headers and the servers configuration.
*
* @return the connection's request configuration
*/
private XmlRpcServerClientRequestData getRequestConfig() {
requestData = new XmlRpcServerClientRequestData(this);
XmlRpcHttpServerConfig serverConfig = (XmlRpcHttpServerConfig) xmlRpcServer.getConfig();
requestData.setBasicEncoding(serverConfig.getBasicEncoding());
requestData.setContentLengthOptional(serverConfig.isContentLengthOptional());
requestData.setEnabledForExtensions(serverConfig.isEnabledForExtensions());
requestData.setEnabledForExceptions(serverConfig.isEnabledForExceptions());
requestData.setMethod("POST");
String httpVersion = request.getProtocolVersion().getText();
requestData.setHttpVersion(httpVersion);
requestData.setKeepAlive(serverConfig.isKeepAliveEnabled() && isKeepAlive(request));
requestData.setContentLength((int) getContentLength(request));
return requestData;
}
@Override
public InputStream newInputStream() throws IOException {
inputStream = new ChannelBufferInputStream(request.getContent());
return new BufferedInputStream(inputStream) {
@Override
public void close() throws IOException {
// Block close. apparently the XML stream parser likes to close
// the stream.
}
};
}
@Override
public OutputStream newOutputStream() throws IOException {
// TODO(keith): Should we check if a ByteArrayOutputStream should
// be returned here? If so, we would need to store it somewhere
// and then properly copy it to the outputStream here.
//
// It is normally used so we can have a content length in the headers.
// Probably not necessary as we are allocating a special output stream,
// not allowing for arbitrary streaming directly on the output channel
return outputStream;
}
@Override
public void close() throws IOException {
if (inputStream != null) {
inputStream.close();
}
}
/**
* Writes an error responses headers to the output stream.
*
* @param requestData
* the request data
* @param throwable
* the error being reported
*
* @throws IOException
* writing the response failed
*/
public void notifyError(XmlRpcServerClientRequestData requestData, Throwable throwable) throws IOException {
handler.getWebServer().getLog().error("Got XMLRPC error!", throwable);
processingThrowable = throwable;
}
/**
* Get the channel buffer containing the response.
*
* <p>
* Once this is called, the output stream used for writing the response is closed.
*
* @return the channel buffer
*/
public ChannelBuffer getChannelBuffer() {
try {
if (outputStream != null) {
outputStream.flush();
}
} catch (IOException e) {
throw new RosRuntimeException("Could not flush XMLRPC server connection output stream", e);
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
throw new RosRuntimeException("Could not close XMLRPC server connection output stream", e);
}
}
}
return channelBuffer;
}
/**
* Set a response HTTP header value.
*
* @param headerName
* the name of the HTTP header
* @param headerValue
* the value of the HTTP header
*/
public void setResponseHeader(String headerName, String headerValue) {
headers.put(headerName, headerValue);
}
}