Invoking a service using a mail

Prologue

Most of the web services that we interact with are synchronous and request-response in nature. However, we see that the synchronous request-response type of interaction is only a part of the messaging scenarios we encounter in real life. Asynchronous messaging is very important in constructing loosely coupled systems. Take for an instance a chain of stores, at the end of the day all the stores all over can send a mail to the central system telling it what that days business activity was and in the morning when the store opens there will be a reply to that mail with new instructions and updates. It is a lot like the way the old business worked but with a modern touch. Similarly Axis2 mail transport can be used to implement SETI@HOME through mail.

Introduction

To get things started you will first need to go through the Mail Transport configuration document. It will give you all the information you need about how to configure Axis2 to get the mail transport working.

Broadly speaking there are 3 ways of calling a service through mail.

1. Using the simple mail server included in Axis2.

2. Using a generic mail server.

3. Using mailets.

Option number 1 and 2 are fairly simple and easy to implement, whereas option 3 is somewhat harder [unless of course you are one of those guys who see mails going around in your dreams.]. However the mailet scenario provides a more robust and useful solution in a production environment.

If you are not a guru in mail related issues you should probably get started on the simple mail server that has been provided with Axis2. Once you get the hang of the Axis2 related issues then you can move onto tackle the mail beast. Please do keep in mind that the mail server we have implemented is ONLY FOR DEMONSTRATION/TESTING PURPOSES.

1. Using the simple mail server included in Axis2

Before we start I want to reiterate the fact that the mail server included with Axis2 is ONLY FOR DEMONSTRATION/TESTING PURPOSES.

The SMTP/POP server that we have included has the ability to function as a standalone SMTP/POP server and also has the ability to work as a mailet. All this is done through a small filter that keeps watch for certain pre-configured email addresses. These pre-configured email addresses can be changed by doing a simple edit of the filter class org.apache.axis2.transport.mail.server.Sorter.

Now that we have the environment set up, let us start pumping out some code to get the mail functionality off the ground. First we'll have a look at it from the mail server side.

        // Start the mail server using the default configurations.
        ConfigurationContext configContext = UtilsMailServer.start();

        // Start the default mail listener. It will starting poling for mail
        // using the configuration from the XML file.
        SimpleMailListener ml = new SimpleMailListener();
        ml.init(configContext, configContext.getAxisConfiguration()
                .getTransportIn(new QName(Constants.TRANSPORT_MAIL)));
        ml.start();

        // Setup a service that will echo what we send to the server.
        AxisService axisService = Utils.createSimpleService(serviceName,
                Echo.class.getName(), new QName("echoOMElement"));
        configContext.getAxisConfiguration().addService(axisService);
        Utils.resolvePhases(configContext.getAxisConfiguration(), axisService);
        ServiceContext serviceContext = configContext
                .createServiceContext(new QName("EchoXMLService"));

This code sets up your Axis2 server working through mail, with a single service. If you need to have a look under the hood have a look at the MailServer and UtilsMailServer classes.

Moving onto the client side have a look at the code listing below. It will call the axisService that was setup on the previous code listing.

        ConfigurationContext configContext = UtilsMailServer
                .createClientConfigurationContext();
        AxisService axisService = new AxisService(serviceName);
        AxisOperation axisOperation = new AxisOperation(operationName);
        axisOperation.setMessageReceiver(new MessageReceiver() {
                public void receive(MessageContext messgeCtx) throws AxisFault {
                        envelope = messgeCtx.getEnvelope();
                }
        });
        axisService.addOperation(axisOperation);
        configContext.getAxisConfiguration().addService(axisService);
        Utils.resolvePhases(configContext.getAxisConfiguration(), axisService);
        ServiceContext serviceContext = configContext
                .createServiceContext(serviceName);
        org.apache.axis2.clientapi.Call call = new org.apache.axis2.clientapi.Call(
                serviceContext);
        call.setTo(targetEPR);
        call.setTransportInfo(Constants.TRANSPORT_MAIL,
                Constants.TRANSPORT_MAIL, true);
        // Create a callback to set to the axisService invocation.
        Callback callback = new Callback() {
                public void onComplete(AsyncResult result) {
                        try {
                                result.getResponseEnvelope().serialize(
                                        XMLOutputFactory.newInstance()
                                                .createXMLStreamWriter(System.out));
                        } catch (XMLStreamException e) {
                                reportError(e);
                        } finally {
                                finish = true;
                        }
                }
                public void reportError(Exception e) {
                        log.info(e.getMessage());
                        finish = true;
                }
         };

        // Call the service and start poling for the reply from the server.
        call.invokeNonBlocking(operationName.getLocalPart(), createEnvelope(),
                callback);
        int index = 0;
        while (!finish) {
                Thread.sleep(1000);
                index++;
                if (index > 10) {
                        throw new AxisFault(
                                "Server is being shutdown as the Async response is taking too long.");
                }
        }
        call.close();

This will call the service that was setup on the server and will poll the mail server till the response is received. Well that is all there is to it.

2. Using a generic mail server

First you need two email accounts that works with POP/SMTP. One will act as a server and the other will act as the client. For the time being we will use server@somewhere.org and client@somewhere.org as the server and the client email addresses. Now that we have the email addresses you will have to set up the client and the server looking at the Mail Transport introduction document.

When calling the generic mail server the client side code will remain the same and there will be some modification to the server-side code.

        // Create a configuration context. This will also load the details about the mail
        // address to listen to from the configuration file.
        File file = new File(MAIL_TRANSPORT_SERVER_ENABLED_REPO_PATH);
        ConfigurationContextFactory builder = new ConfigurationContextFactory();
        ConfigurationContext configContextbuilder
                .buildConfigurationContext(file.getAbsolutePath());

        // Startup the default mail server and start listening to a 
        // mail address.
        SimpleMailListener ml = new SimpleMailListener();
        ml.init(configContext, configContext.getAxisConfiguration()
                .getTransportIn(new QName(Constants.TRANSPORT_MAIL)));
        ml.start();

        // Setup a simple service.
        AxisService axisService = Utils.createSimpleService(serviceName,
                Echo.class.getName(), operationName);
        configContext.getAxisConfiguration().addService(axisService);
        Utils.resolvePhases(configContext.getAxisConfiguration(), axisService);
        ServiceContext serviceContext = configContext.createServiceContext(serviceName);

We have to create a separate ConfigurationContext and then use that. We are done; wasn't that simple?

3. Calling Axis through a James mailet

This process will be a bit more challenging than the other two methods but will provide a really elegant way to use the mail transport. Before we get started you will have to go though the James documents Writing a Custom Matcher and Writing a Custom Mailet.

Now that we know the James part of it lets dive into to the Axis2 part of the code. Once you have set up the James side of business we just need to use the same functionality that is used in the above code. Have a look at the code listing below for more details

    public void processMail(ConfigurationContext confContext, MimeMessage mimeMessage) {
        // create an Axis server
        AxisEngine engine = new AxisEngine(confContext);
        MessageContext msgContext = null;
        // create and initialize a message context
        try {
            // Create a message context with mail as in and out transports.
            msgContext =
                    new MessageContext(confContext,
                            confContext.getAxisConfiguration().getTransportIn(new QName(Constants.TRANSPORT_MAIL)),
                            confContext.getAxisConfiguration().getTransportOut(new QName(Constants.TRANSPORT_MAIL)));
            msgContext.setServerSide(true);

            msgContext.setProperty(MailConstants.CONTENT_TYPE, mimeMessage.getContentType());
            msgContext.setWSAAction(getMailHeader(MailConstants.HEADER_SOAP_ACTION, mimeMessage));

            // The service path is in the subject of the mail.
            String serviceURL = mimeMessage.getSubject();
            if (serviceURL == null) {
                serviceURL = "";
            }

            String replyTo = ((InternetAddress) mimeMessage.getReplyTo()[0]).getAddress();
            if (replyTo != null) {
                msgContext.setReplyTo(new EndpointReference(replyTo));
            }

            String recipients = ((InternetAddress) mimeMessage.getAllRecipients()[0]).getAddress();

            if (recipients != null) {
                msgContext.setTo(new EndpointReference(recipients + "/" + serviceURL));
            }

            // add the SOAPEnvelope
            String message = mimeMessage.getContent().toString();
            ByteArrayInputStream bais = new ByteArrayInputStream(message.getBytes());
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(bais);

            // This is just in place to work with SOAP 1.1 and 1.2.
            String soapNamespaceURI = "";
            if (mimeMessage.getContentType().indexOf(SOAP12Constants.SOAP_12_CONTENT_TYPE) > -1) {
                soapNamespaceURI = SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI;
            } else if (mimeMessage.getContentType().indexOf(SOAP11Constants.SOAP_11_CONTENT_TYPE) > -1) {
                soapNamespaceURI = SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI;

            }
            StAXBuilder builder = new StAXSOAPModelBuilder(reader, soapNamespaceURI);

            SOAPEnvelope envelope = (SOAPEnvelope) builder.getDocumentElement();
            msgContext.setEnvelope(envelope);
            if (envelope.getBody().hasFault()) {
                engine.receiveFault(msgContext);
            } else {
                engine.receive(msgContext);
            }
        } catch (Exception e) {
            try {
                if (msgContext != null) {
                    MessageContext faultContext = engine.createFaultMessageContext(msgContext, e);
                    engine.sendFault(faultContext);
                }
            } catch (Exception e1) {
                log.error(e);
            }
        }
    }

If you don't happen to have a ConfigurationContext lying around to call this method you can use the following bit of code to get one. Once you create one you can store that on the mailet and keep using it.

        File file = new File(MAIL_TRANSPORT_SERVER_ENABLED_REPO_PATH);
        ConfigurationContextFactory builder = new ConfigurationContextFactory();
        ConfigurationContext configContextbuilder
                .buildConfigurationContext(file.getAbsolutePath());

Well that seems to be all from the wonderful world of mail for now. :)