/* * 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); } }