001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.cocoon.reading;
019:
020: import java.io.ByteArrayOutputStream;
021: import java.io.IOException;
022: import java.io.OutputStream;
023: import java.util.Map;
024:
025: import javax.servlet.ServletContext;
026: import javax.servlet.http.HttpServletRequest;
027: import javax.servlet.http.HttpServletResponse;
028: import javax.servlet.http.HttpUtils;
029: import javax.xml.soap.SOAPException;
030:
031: import org.apache.avalon.framework.activity.Disposable;
032: import org.apache.avalon.framework.configuration.Configurable;
033: import org.apache.avalon.framework.configuration.Configuration;
034: import org.apache.avalon.framework.configuration.ConfigurationException;
035: import org.apache.avalon.framework.parameters.Parameters;
036: import org.apache.avalon.framework.service.ServiceException;
037: import org.apache.avalon.framework.service.ServiceManager;
038:
039: import org.apache.axis.AxisFault;
040: import org.apache.axis.Constants;
041: import org.apache.axis.Message;
042: import org.apache.axis.MessageContext;
043: import org.apache.axis.soap.SOAPConstants;
044: import org.apache.axis.transport.http.AxisHttpSession;
045: import org.apache.axis.transport.http.HTTPConstants;
046:
047: import org.apache.cocoon.ProcessingException;
048: import org.apache.cocoon.components.axis.SoapServer;
049: import org.apache.cocoon.environment.ObjectModelHelper;
050: import org.apache.cocoon.environment.SourceResolver;
051: import org.apache.cocoon.environment.http.HttpEnvironment;
052:
053: import org.w3c.dom.Element;
054: import org.xml.sax.SAXException;
055:
056: /**
057: * SOAP Reader
058: *
059: * <p>
060: * This reader accepts a SOAP Request, and generates the resultant
061: * response as output. Essentially, this reader allows you to serve SOAP
062: * requests from your Cocoon application.
063: * </p>
064: *
065: * <p>
066: * Code originates from the Apache
067: * <a href="http://xml.apache.org/axis">AXIS</a> project,
068: * <code>org.apache.axis.http.transport.AxisServlet</code>.
069: * </p>
070: *
071: * Ported to Cocoon by:
072: *
073: * @author <a href="mailto:crafterm@apache.org">Marcus Crafter</a>
074: *
075: * Original <code>AxisServlet</code> authors:
076: *
077: * @author <a href="mailto:">Steve Loughran</a>
078: * @author <a href="mailto:dug@us.ibm.com">Doug Davis</a>
079: *
080: * @version CVS $Id: AxisRPCReader.java 433543 2006-08-22 06:22:54Z crossley $
081: */
082: public class AxisRPCReader extends ServiceableReader implements
083: Configurable, Disposable {
084:
085: // soap server reference
086: private SoapServer m_server;
087:
088: /** Are we in development stage ? */
089: private boolean m_isDevelompent = false;
090:
091: /* (non-Javadoc)
092: * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
093: */
094: public void configure(Configuration config)
095: throws ConfigurationException {
096: m_isDevelompent = config.getChild("development-stage")
097: .getValueAsBoolean(m_isDevelompent);
098: }
099:
100: public void service(final ServiceManager manager)
101: throws ServiceException {
102: super .service(manager);
103: // set soap server reference
104: m_server = (SoapServer) manager.lookup(SoapServer.ROLE);
105: }
106:
107: /**
108: * Axis RPC Router <code>setup</code> method.
109: *
110: * <p>
111: * This method sets the reader up for use. Essentially it checks that
112: * its been invoked in a HTTP-POST environment, reads some optional
113: * configuration variables, and obtains several component references to
114: * be used later.
115: * </p>
116: *
117: * @param resolver <code>SourceResolver</code> instance
118: * @param objectModel request/response/context data
119: * @param src source <code>String</code> instance
120: * @param parameters sitemap invocation time customization parameters
121: * @exception ProcessingException if an error occurs
122: * @exception IOException if an error occurs
123: * @exception SAXException if an error occurs
124: */
125: public void setup(final SourceResolver resolver,
126: final Map objectModel, final String src,
127: final Parameters parameters) throws ProcessingException,
128: IOException, SAXException {
129: super .setup(resolver, objectModel, src, parameters);
130:
131: checkHTTPPost(objectModel);
132:
133: if (getLogger().isDebugEnabled()) {
134: getLogger().debug("AxisRPCReader.setup() complete");
135: }
136: }
137:
138: /**
139: * Helper method to ensure that given a HTTP-POST.
140: *
141: * @param objectModel Request/Response/Context map.
142: * @exception ProcessingException if a non HTTP-POST request has been made.
143: */
144: private void checkHTTPPost(final Map objectModel)
145: throws ProcessingException {
146: String method = ObjectModelHelper.getRequest(objectModel)
147: .getMethod();
148:
149: if (!"POST".equalsIgnoreCase(method))
150: throw new ProcessingException(
151: "Reader only supports HTTP-POST (supplied was "
152: + method + ")");
153: }
154:
155: /**
156: * Axis RPC Router <code>generate</code> method.
157: *
158: * <p>
159: * This method reads the SOAP request in from the input stream, invokes
160: * the requested method and sends the result back to the requestor
161: * </p>
162: *
163: * @exception IOException if an IO error occurs
164: * @exception SAXException if a SAX error occurs
165: * @exception ProcessingException if a processing error occurs
166: */
167: public void generate() throws IOException, SAXException,
168: ProcessingException {
169: HttpServletRequest req = (HttpServletRequest) objectModel
170: .get(HttpEnvironment.HTTP_REQUEST_OBJECT);
171: HttpServletResponse res = (HttpServletResponse) objectModel
172: .get(HttpEnvironment.HTTP_RESPONSE_OBJECT);
173: ServletContext con = (ServletContext) objectModel
174: .get(HttpEnvironment.HTTP_SERVLET_CONTEXT);
175:
176: String soapAction = null;
177: MessageContext msgContext = null;
178: Message responseMsg = null;
179:
180: try {
181: res.setBufferSize(1024 * 8); // provide performance boost.
182:
183: // Get message context w/ various properties set
184: msgContext = m_server.createMessageContext(req, res, con);
185:
186: // Get request message
187: Message requestMsg = new Message(
188: req.getInputStream(),
189: false,
190: req.getHeader(HTTPConstants.HEADER_CONTENT_TYPE),
191: req
192: .getHeader(HTTPConstants.HEADER_CONTENT_LOCATION));
193:
194: if (getLogger().isDebugEnabled()) {
195: getLogger().debug(
196: "Request message:\n"
197: + messageToString(requestMsg));
198: }
199:
200: // Set the request(incoming) message field in the context
201: msgContext.setRequestMessage(requestMsg);
202: String url = HttpUtils.getRequestURL(req).toString();
203: msgContext.setProperty(MessageContext.TRANS_URL, url);
204:
205: try {
206: //
207: // Save the SOAPAction header in the MessageContext bag.
208: // This will be used to tell the Axis Engine which service
209: // is being invoked. This will save us the trouble of
210: // having to parse the Request message - although we will
211: // need to double-check later on that the SOAPAction header
212: // does in fact match the URI in the body.
213: // (is this last stmt true??? (I don't think so - Glen))
214: //
215: soapAction = getSoapAction(req);
216:
217: if (soapAction != null) {
218: msgContext.setUseSOAPAction(true);
219: msgContext.setSOAPActionURI(soapAction);
220: }
221:
222: // Create a Session wrapper for the HTTP session.
223: msgContext.setSession(new AxisHttpSession(req));
224:
225: // Invoke the Axis engine...
226: if (getLogger().isDebugEnabled()) {
227: getLogger().debug("Invoking Axis Engine");
228: }
229:
230: m_server.invoke(msgContext);
231:
232: if (getLogger().isDebugEnabled()) {
233: getLogger().debug("Return from Axis Engine");
234: }
235:
236: responseMsg = msgContext.getResponseMessage();
237: if (responseMsg == null) {
238: //tell everyone that something is wrong
239: throw new Exception("no response message");
240: }
241: } catch (AxisFault fault) {
242: if (getLogger().isErrorEnabled()) {
243: getLogger().error("Axis Fault", fault);
244: }
245:
246: // log and sanitize
247: processAxisFault(fault);
248: configureResponseFromAxisFault(res, fault);
249: responseMsg = msgContext.getResponseMessage();
250: if (responseMsg == null) {
251: responseMsg = new Message(fault);
252: }
253: } catch (Exception e) {
254: //other exceptions are internal trouble
255: responseMsg = msgContext.getResponseMessage();
256: res
257: .setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
258: if (getLogger().isErrorEnabled()) {
259: getLogger().error("Error during SOAP call", e);
260: }
261: if (responseMsg == null) {
262: AxisFault fault = AxisFault.makeFault(e);
263: processAxisFault(fault);
264: responseMsg = new Message(fault);
265: }
266: }
267: } catch (AxisFault fault) {
268: if (getLogger().isErrorEnabled()) {
269: getLogger().error(
270: "Axis fault occured while perforing request",
271: fault);
272: }
273: processAxisFault(fault);
274: configureResponseFromAxisFault(res, fault);
275: responseMsg = msgContext.getResponseMessage();
276: if (responseMsg == null) {
277: responseMsg = new Message(fault);
278: }
279: } catch (Exception e) {
280: throw new ProcessingException(
281: "Exception thrown while performing request", e);
282: }
283:
284: // Send response back
285: if (responseMsg != null) {
286: if (getLogger().isDebugEnabled()) {
287: getLogger().debug(
288: "Sending response:\n"
289: + messageToString(responseMsg));
290: }
291:
292: sendResponse(getProtocolVersion(req), msgContext
293: .getSOAPConstants(), res, responseMsg);
294: }
295:
296: if (getLogger().isDebugEnabled()) {
297: getLogger().debug("AxisRPCReader.generate() complete");
298: }
299: }
300:
301: /**
302: * routine called whenever an axis fault is caught; where they
303: * are logged and any other business. The method may modify the fault
304: * in the process
305: * @param fault what went wrong.
306: */
307: protected void processAxisFault(AxisFault fault) {
308: //log the fault
309: Element runtimeException = fault
310: .lookupFaultDetail(Constants.QNAME_FAULTDETAIL_RUNTIMEEXCEPTION);
311: if (runtimeException != null) {
312: getLogger().info("AxisFault:", fault);
313: //strip runtime details
314: fault
315: .removeFaultDetail(Constants.QNAME_FAULTDETAIL_RUNTIMEEXCEPTION);
316: } else if (getLogger().isDebugEnabled()) {
317: getLogger().debug("AxisFault:", fault);
318: }
319: //dev systems only give fault dumps
320: if (m_isDevelompent) {
321: //strip out the stack trace
322: fault
323: .removeFaultDetail(Constants.QNAME_FAULTDETAIL_STACKTRACE);
324: }
325: }
326:
327: /**
328: * Configure the servlet response status code and maybe other headers
329: * from the fault info.
330: * @param response response to configure
331: * @param fault what went wrong
332: */
333: private void configureResponseFromAxisFault(
334: HttpServletResponse response, AxisFault fault) {
335: // then get the status code
336: // It's been suggested that a lack of SOAPAction
337: // should produce some other error code (in the 400s)...
338: int status = getHttpServletResponseStatus(fault);
339: if (status == HttpServletResponse.SC_UNAUTHORIZED) {
340: // unauth access results in authentication request
341: // TODO: less generic realm choice?
342: response.setHeader("WWW-Authenticate",
343: "Basic realm=\"AXIS\"");
344: }
345: response.setStatus(status);
346: }
347:
348: /**
349: * Extract information from AxisFault and map it to a HTTP Status code.
350: *
351: * @param af Axis Fault
352: * @return HTTP Status code.
353: */
354: protected int getHttpServletResponseStatus(AxisFault af) {
355: // This will raise a 401 for both "Unauthenticated" & "Unauthorized"...
356: return af.getFaultCode().getLocalPart().startsWith(
357: "Server.Unauth") ? HttpServletResponse.SC_UNAUTHORIZED
358: : HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
359: }
360:
361: /**
362: * write a message to the response, set appropriate headers for content
363: * type..etc.
364: * @param clientVersion client protocol, one of the HTTPConstants strings
365: * @param res response
366: * @param responseMsg message to write
367: * @throws AxisFault
368: * @throws IOException if the response stream can not be written to
369: */
370: private void sendResponse(final String clientVersion,
371: final SOAPConstants constants,
372: final HttpServletResponse res, final Message responseMsg)
373: throws AxisFault, IOException {
374: if (responseMsg == null) {
375: res.setStatus(HttpServletResponse.SC_NO_CONTENT);
376:
377: if (getLogger().isDebugEnabled()) {
378: getLogger().debug("No axis response, not sending one");
379: }
380: } else {
381: if (getLogger().isDebugEnabled()) {
382: getLogger()
383: .debug(
384: "Returned Content-Type:"
385: + responseMsg
386: .getContentType(constants));
387: getLogger().debug(
388: "Returned Content-Length:"
389: + responseMsg.getContentLength());
390: }
391:
392: try {
393: res.setContentType(responseMsg
394: .getContentType(constants));
395: responseMsg.writeTo(res.getOutputStream());
396: } catch (SOAPException e) {
397: getLogger().error("Exception sending response", e);
398: }
399: }
400:
401: if (!res.isCommitted()) {
402: res.flushBuffer(); // Force it right now.
403: }
404: }
405:
406: /**
407: * Extract the SOAPAction header.
408: * if SOAPAction is null then we'll we be forced to scan the body for it.
409: * if SOAPAction is "" then use the URL
410: * @param req incoming request
411: * @return the action
412: * @throws AxisFault
413: */
414: private String getSoapAction(HttpServletRequest req)
415: throws AxisFault {
416: String soapAction = req
417: .getHeader(HTTPConstants.HEADER_SOAP_ACTION);
418:
419: if (getLogger().isDebugEnabled()) {
420: getLogger().debug("HEADER_SOAP_ACTION:" + soapAction);
421: }
422:
423: //
424: // Technically, if we don't find this header, we should probably fault.
425: // It's required in the SOAP HTTP binding.
426: //
427: if (soapAction == null) {
428: throw new AxisFault("Client.NoSOAPAction",
429: "No SOAPAction header", null, null);
430: }
431:
432: if (soapAction.length() == 0)
433: soapAction = req.getContextPath(); // Is this right?
434:
435: return soapAction;
436: }
437:
438: /**
439: * Return the HTTP protocol level 1.1 or 1.0
440: * by derived class.
441: */
442: private String getProtocolVersion(HttpServletRequest req) {
443: String ret = HTTPConstants.HEADER_PROTOCOL_V10;
444: String prot = req.getProtocol();
445: if (prot != null) {
446: int sindex = prot.indexOf('/');
447: if (-1 != sindex) {
448: String ver = prot.substring(sindex + 1);
449: if (HTTPConstants.HEADER_PROTOCOL_V11
450: .equals(ver.trim())) {
451: ret = HTTPConstants.HEADER_PROTOCOL_V11;
452: }
453: }
454: }
455: return ret;
456: }
457:
458: /**
459: * Helper method to convert a <code>Message</code> structure
460: * into a <code>String</code>.
461: *
462: * @param msg a <code>Message</code> value
463: * @return a <code>String</code> value
464: */
465: private String messageToString(final Message msg) {
466: try {
467: OutputStream os = new ByteArrayOutputStream();
468: msg.writeTo(os);
469: return os.toString();
470: } catch (Exception e) {
471: if (getLogger().isWarnEnabled()) {
472: getLogger().warn(
473: "Warning, could not convert message (" + msg
474: + ") into string", e);
475: }
476:
477: return null;
478: }
479: }
480:
481: /**
482: * Dispose this reader. Release all held resources.
483: */
484: public void dispose() {
485: manager.release(m_server);
486: }
487: }
|