blob: 00b84896985fdfb84a54c1092b422436a8e8cf74 [file] [log] [blame]
/*
* 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.apache.cocoon.reading;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpUtils;
import javax.xml.soap.SOAPException;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.soap.SOAPConstants;
import org.apache.axis.transport.http.AxisHttpSession;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.components.axis.SoapServer;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.environment.http.HttpEnvironment;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
/**
* SOAP Reader
*
* <p>
* This reader accepts a SOAP Request, and generates the resultant
* response as output. Essentially, this reader allows you to serve SOAP
* requests from your Cocoon application.
* </p>
*
* <p>
* Code originates from the Apache
* <a href="http://xml.apache.org/axis">AXIS</a> project,
* <code>org.apache.axis.http.transport.AxisServlet</code>.
* </p>
*
* Ported to Cocoon by:
*
* @author <a href="mailto:crafterm@apache.org">Marcus Crafter</a>
*
* Original <code>AxisServlet</code> authors:
*
* @author <a href="mailto:">Steve Loughran</a>
* @author <a href="mailto:dug@us.ibm.com">Doug Davis</a>
*
* @version CVS $Id$
*/
public class AxisRPCReader extends ServiceableReader
implements Configurable, Disposable {
// soap server reference
private SoapServer m_server;
/** Are we in development stage ? */
private boolean m_isDevelompent = false;
/* (non-Javadoc)
* @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
*/
public void configure(Configuration config) throws ConfigurationException {
m_isDevelompent = config.getChild("development-stage").getValueAsBoolean(m_isDevelompent );
}
public void service(final ServiceManager manager) throws ServiceException {
super.service(manager);
// set soap server reference
m_server = (SoapServer) manager.lookup(SoapServer.ROLE);
}
/**
* Axis RPC Router <code>setup</code> method.
*
* <p>
* This method sets the reader up for use. Essentially it checks that
* its been invoked in a HTTP-POST environment, reads some optional
* configuration variables, and obtains several component references to
* be used later.
* </p>
*
* @param resolver <code>SourceResolver</code> instance
* @param objectModel request/response/context data
* @param src source <code>String</code> instance
* @param parameters sitemap invocation time customization parameters
* @exception ProcessingException if an error occurs
* @exception IOException if an error occurs
* @exception SAXException if an error occurs
*/
public void setup(
final SourceResolver resolver,
final Map objectModel,
final String src,
final Parameters parameters)
throws ProcessingException, IOException, SAXException {
super.setup(resolver, objectModel, src, parameters);
checkHTTPPost(objectModel);
if (getLogger().isDebugEnabled()) {
getLogger().debug("AxisRPCReader.setup() complete");
}
}
/**
* Helper method to ensure that given a HTTP-POST.
*
* @param objectModel Request/Response/Context map.
* @exception ProcessingException if a non HTTP-POST request has been made.
*/
private void checkHTTPPost(final Map objectModel)
throws ProcessingException {
String method = ObjectModelHelper.getRequest(objectModel).getMethod();
if (!"POST".equalsIgnoreCase(method))
throw new ProcessingException(
"Reader only supports HTTP-POST (supplied was " + method + ")"
);
}
/**
* Axis RPC Router <code>generate</code> method.
*
* <p>
* This method reads the SOAP request in from the input stream, invokes
* the requested method and sends the result back to the requestor
* </p>
*
* @exception IOException if an IO error occurs
* @exception SAXException if a SAX error occurs
* @exception ProcessingException if a processing error occurs
*/
public void generate()
throws IOException, SAXException, ProcessingException {
HttpServletRequest req =
(HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
HttpServletResponse res =
(HttpServletResponse) objectModel.get(HttpEnvironment.HTTP_RESPONSE_OBJECT);
ServletContext con =
(ServletContext) objectModel.get(HttpEnvironment.HTTP_SERVLET_CONTEXT);
String soapAction = null;
MessageContext msgContext = null;
Message responseMsg = null;
try {
res.setBufferSize(1024 * 8); // provide performance boost.
// Get message context w/ various properties set
msgContext = m_server.createMessageContext(req, res, con);
// Get request message
Message requestMsg =
new Message(
req.getInputStream(), false,
req.getHeader(HTTPConstants.HEADER_CONTENT_TYPE),
req.getHeader(HTTPConstants.HEADER_CONTENT_LOCATION)
);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Request message:\n" + messageToString(requestMsg));
}
// Set the request(incoming) message field in the context
msgContext.setRequestMessage(requestMsg);
String url = HttpUtils.getRequestURL(req).toString();
msgContext.setProperty(MessageContext.TRANS_URL, url);
try {
//
// Save the SOAPAction header in the MessageContext bag.
// This will be used to tell the Axis Engine which service
// is being invoked. This will save us the trouble of
// having to parse the Request message - although we will
// need to double-check later on that the SOAPAction header
// does in fact match the URI in the body.
// (is this last stmt true??? (I don't think so - Glen))
//
soapAction = getSoapAction(req);
if (soapAction != null) {
msgContext.setUseSOAPAction(true);
msgContext.setSOAPActionURI(soapAction);
}
// Create a Session wrapper for the HTTP session.
msgContext.setSession(new AxisHttpSession(req));
// Invoke the Axis engine...
if(getLogger().isDebugEnabled()) {
getLogger().debug("Invoking Axis Engine");
}
m_server.invoke(msgContext);
if(getLogger().isDebugEnabled()) {
getLogger().debug("Return from Axis Engine");
}
responseMsg = msgContext.getResponseMessage();
if (responseMsg == null) {
//tell everyone that something is wrong
throw new Exception("no response message");
}
} catch (AxisFault fault) {
if (getLogger().isErrorEnabled()) {
getLogger().error("Axis Fault", fault);
}
// log and sanitize
processAxisFault(fault);
configureResponseFromAxisFault(res, fault);
responseMsg = msgContext.getResponseMessage();
if (responseMsg == null) {
responseMsg = new Message(fault);
}
} catch (Exception e) {
//other exceptions are internal trouble
responseMsg = msgContext.getResponseMessage();
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
if (getLogger().isErrorEnabled()) {
getLogger().error("Error during SOAP call", e);
}
if (responseMsg == null) {
AxisFault fault = AxisFault.makeFault(e);
processAxisFault(fault);
responseMsg = new Message(fault);
}
}
} catch (AxisFault fault) {
if (getLogger().isErrorEnabled()) {
getLogger().error("Axis fault occured while perforing request", fault);
}
processAxisFault(fault);
configureResponseFromAxisFault(res, fault);
responseMsg = msgContext.getResponseMessage();
if( responseMsg == null) {
responseMsg = new Message(fault);
}
} catch (Exception e) {
throw new ProcessingException("Exception thrown while performing request", e);
}
// Send response back
if (responseMsg != null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Sending response:\n" + messageToString(responseMsg));
}
sendResponse(getProtocolVersion(req), msgContext.getSOAPConstants(), res, responseMsg);
}
if (getLogger().isDebugEnabled()) {
getLogger().debug("AxisRPCReader.generate() complete");
}
}
/**
* routine called whenever an axis fault is caught; where they
* are logged and any other business. The method may modify the fault
* in the process
* @param fault what went wrong.
*/
protected void processAxisFault(AxisFault fault) {
//log the fault
Element runtimeException = fault.lookupFaultDetail(
Constants.QNAME_FAULTDETAIL_RUNTIMEEXCEPTION);
if (runtimeException != null) {
getLogger().info("AxisFault:", fault);
//strip runtime details
fault.removeFaultDetail(Constants.QNAME_FAULTDETAIL_RUNTIMEEXCEPTION);
} else if (getLogger().isDebugEnabled()) {
getLogger().debug("AxisFault:", fault);
}
//dev systems only give fault dumps
if (m_isDevelompent) {
//strip out the stack trace
fault.removeFaultDetail(Constants.QNAME_FAULTDETAIL_STACKTRACE);
}
}
/**
* Configure the servlet response status code and maybe other headers
* from the fault info.
* @param response response to configure
* @param fault what went wrong
*/
private void configureResponseFromAxisFault(HttpServletResponse response,
AxisFault fault) {
// then get the status code
// It's been suggested that a lack of SOAPAction
// should produce some other error code (in the 400s)...
int status = getHttpServletResponseStatus(fault);
if (status == HttpServletResponse.SC_UNAUTHORIZED) {
// unauth access results in authentication request
// TODO: less generic realm choice?
response.setHeader("WWW-Authenticate","Basic realm=\"AXIS\"");
}
response.setStatus(status);
}
/**
* Extract information from AxisFault and map it to a HTTP Status code.
*
* @param af Axis Fault
* @return HTTP Status code.
*/
protected int getHttpServletResponseStatus(AxisFault af) {
// This will raise a 401 for both "Unauthenticated" & "Unauthorized"...
return af.getFaultCode().getLocalPart().startsWith("Server.Unauth")
? HttpServletResponse.SC_UNAUTHORIZED
: HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
}
/**
* write a message to the response, set appropriate headers for content
* type..etc.
* @param clientVersion client protocol, one of the HTTPConstants strings
* @param res response
* @param responseMsg message to write
* @throws AxisFault
* @throws IOException if the response stream can not be written to
*/
private void sendResponse(
final String clientVersion,
final SOAPConstants constants,
final HttpServletResponse res,
final Message responseMsg)
throws AxisFault, IOException {
if (responseMsg == null) {
res.setStatus(HttpServletResponse.SC_NO_CONTENT);
if (getLogger().isDebugEnabled()) {
getLogger().debug("No axis response, not sending one");
}
} else {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Returned Content-Type:" + responseMsg.getContentType(constants));
getLogger().debug("Returned Content-Length:" + responseMsg.getContentLength());
}
try {
res.setContentType(responseMsg.getContentType(constants));
responseMsg.writeTo(res.getOutputStream());
} catch (SOAPException e) {
getLogger().error("Exception sending response", e);
}
}
if (!res.isCommitted()) {
res.flushBuffer(); // Force it right now.
}
}
/**
* Extract the SOAPAction header.
* if SOAPAction is null then we'll we be forced to scan the body for it.
* if SOAPAction is "" then use the URL
* @param req incoming request
* @return the action
* @throws AxisFault
*/
private String getSoapAction(HttpServletRequest req)
throws AxisFault {
String soapAction = req.getHeader(HTTPConstants.HEADER_SOAP_ACTION);
if (getLogger().isDebugEnabled()) {
getLogger().debug("HEADER_SOAP_ACTION:" + soapAction);
}
//
// Technically, if we don't find this header, we should probably fault.
// It's required in the SOAP HTTP binding.
//
if (soapAction == null) {
throw new AxisFault(
"Client.NoSOAPAction",
"No SOAPAction header",
null, null
);
}
if (soapAction.length() == 0)
soapAction = req.getContextPath(); // Is this right?
return soapAction;
}
/**
* Return the HTTP protocol level 1.1 or 1.0
* by derived class.
*/
private String getProtocolVersion(HttpServletRequest req) {
String ret = HTTPConstants.HEADER_PROTOCOL_V10;
String prot = req.getProtocol();
if (prot!= null) {
int sindex= prot.indexOf('/');
if (-1 != sindex) {
String ver= prot.substring(sindex+1);
if (HTTPConstants.HEADER_PROTOCOL_V11.equals(ver.trim())) {
ret = HTTPConstants.HEADER_PROTOCOL_V11;
}
}
}
return ret;
}
/**
* Helper method to convert a <code>Message</code> structure
* into a <code>String</code>.
*
* @param msg a <code>Message</code> value
* @return a <code>String</code> value
*/
private String messageToString(final Message msg) {
try {
OutputStream os = new ByteArrayOutputStream();
msg.writeTo(os);
return os.toString();
} catch (Exception e) {
if (getLogger().isWarnEnabled()) {
getLogger().warn(
"Warning, could not convert message (" + msg + ") into string", e
);
}
return null;
}
}
/**
* Dispose this reader. Release all held resources.
*/
public void dispose() {
manager.release(m_server);
}
}