воскресенье, 25 апреля 2010 г.

ECF: Пишем SOAP-клиента на примере использования веб-сервиса "Аэрофлота"


В версии 3.2 Eclipse Communication Framework появилась возможность разрабатывать клиенты к SOAP-сервисам (до этого была возможность использовать только REST-сервисы), используя Remote Services API. Инкапсулирована данная возможность в бандле org.eclipse.ecf.remoteservice.soap.

Есть одно радикальное отличие в реализации ECF-клиента к SOAP веб-сервису от реализации клиентов к другим типам удаленных сервисов. Дело в том, что бандл org.eclipse.ecf.remoteservice.soap содержит лишь набор неких базовых классов, конкретный же контейнер, представляющий собой клиента к конкретному веб-сервису, придется писать самостоятельно. Точно так же самостоятельно придется реализовывать непосредственно логику обращения к веб-сервису, используя для этого такие библиотеки, как Axis, Axis 2 или XFire.

Давайте рассмотрим пример - реализуем бандл, name.samolisov.ecf.webservices.demo, который будет содержать удаленный сервис, инкапсулирующий обращение к некоторым методам веб-сервиса компании "Аэрофлот". WSDL-описание веб-сервиса компании "Аэрофлот" расположено здесь. Данный сервис предоставляет информацию об аэропортах, рейсах, предоставляет табло прилетов и табло вылетов. Мы реализуем обращение к следующим методам: AirportList - предоставляет список аэропортов, в/из которые/ых летают самолеты "Аэрофлота", AirportInfo - возвращает информацию об аэропорте по его коду и метод Arrival - принимает код эропорта, дату, поле по которому будет осуществляться сортировка и направление сортировки и возвращает табло прилетов для данного аэропорта на данную дату, отсортированное по указанным критериям. Мы реализуем обращение к данному методу, с сортировкой по аэропорту по возрастанию.



Итак. Прежде всего необходимо подключить библиотеку доступа к веб-сервису и сгенерировать соответствующие заглушки. Будем использовать библиотеку Apache Axis. В бандле необходимо создать каталог lib, в который поместить следующие файлы:

- axis.jar
- commons-discovery.jar
- commons-logging.jar (вообще, логирование можно подключить отдельным бандлом, но не будем усложнять картину)
- jaxrpc.jar
- saaj.jar
- wsdl4j.jar

Чтобы данные файлы были видны в OSGi-контейнере, их необходимо добавить в секцию Bundle-ClassPath манифеста бандла:

  1. Bundle-ClassPath: lib/axis.jar,

  2.  lib/commons-discovery-0.2.jar,

  3.  lib/commons-logging-1.0.4.jar,

  4.  lib/jaxrpc.jar,

  5.  lib/saaj.jar,

  6.  lib/wsdl4j-1.5.1.jar,

  7.  .



Теперь нужно сгенерировать java-заглушки, через которые будет вестись общение с веб-сервисом "Аэрофлота". Сделать это можно или с помощью ант-скрипта, использующего wsdl4j, или с помощью, входящей в состав Eclipse WTP утилиты. Пользоваться утилитой очень просто: необходимо выбрать пункт меню New->Web Services->Web Service Client, на появившейся форме ввести урл WSDL-описания веб-сервиса и, в общих чертах, все. Будет сгенерирован пакет ru.aeroflot.www, в котором окажутся классы, инкапсулирующие записи, передаваемые по веб-сервису и необходимые прокси.

Приступим непосредственно к разработке бандла. Нам необходимо реализовать следующее:

1. Интерфейс сервиса - интерфейс, в котором определены методы, вызываемые пользователями бандла. Фактически, ECF по данному интерфейсу построит прокси, заменяющий вызовы методов на обращения к веб-сервису. Создадим интерфейс IFlightStatusService, код которого следующий:

package name.samolisov.ecf.webservices.demo;



import java.util.Date;

import java.util.List;



import ru.aeroflot.www.Airport;

import ru.aeroflot.www.Flight;



public interface IFlightStatusService

{

    List<Airport> getAirPortList();



    Airport getAirPort(String code);



    List<Flight> getArrivalOrderByAirPort(String airportCode, Date date);

}

 


2. Класс-наследник от AbstractSoapClientService, который реализует непосредственное обращение к веб-сервису. Необходимо переопределить метод invokeRemoteCall, принимающий два параметра: экземпляр класса IRemoteCall (инкапсулирует конкретный вызов сервиса - значения параметров методов и т.д.) и экземпляр класса IRemoteCallable (инкапсулирует определение вызова - имя метода, какие-то стандартные параметры, урлы и т.д.). В данном методе осуществляется диспетчеризация и, в зависимости от вызываемого метода интерфейса IFlightStatusService, происходит обращение к тому или иному методу веб-сервиса. Создадим класс AeroflotSoapClientService, код которого следующий:

package name.samolisov.ecf.webservices.demo;



import java.rmi.RemoteException;

import java.text.DateFormat;

import java.util.Arrays;

import java.util.Date;



import javax.xml.rpc.ServiceException;



import org.eclipse.ecf.core.util.ECFException;

import org.eclipse.ecf.remoteservice.IRemoteCall;

import org.eclipse.ecf.remoteservice.client.AbstractClientContainer;

import org.eclipse.ecf.remoteservice.client.IRemoteCallable;

import org.eclipse.ecf.remoteservice.client.RemoteServiceClientRegistration;

import org.eclipse.ecf.remoteservice.soap.client.AbstractSoapClientService;



import ru.aeroflot.www.FlightStatusLocator;



public class AeroflotSoapClientService extends AbstractSoapClientService

{

    public AeroflotSoapClientService(AbstractClientContainer container, RemoteServiceClientRegistration registration)

    {

        super(container, registration);

    }



    @Override

    protected Object invokeRemoteCall(IRemoteCall call, IRemoteCallable callable) throws ECFException

    {

        // Dispatch methods by names

        if (AeroflotSoapClientContainer.AIRPORT_LIST_METHOD.equals(callable.getMethod()))

        {

            // Setup and make remote call via axis client

            try

            {

                // Now make blocking remote call

                return Arrays.asList(new FlightStatusLocator().getFlightStatusSoap12().airportList());

            }

            catch (ServiceException e)

            {

                handleInvokeException("Exception setting up SOAP call", e);

            }

            catch (RemoteException e)

            {

                handleInvokeException("Exception setting up SOAP call", e);

            }

        }

        else if (AeroflotSoapClientContainer.AIRPORT_INFO_METHOD.equals(callable.getMethod()))

        {

            try

            {

                return new FlightStatusLocator().getFlightStatusSoap12().airportInfo((String) call.getParameters()[0]);

            }

            catch (ServiceException e)

            {

                handleInvokeException("Exception setting up SOAP call", e);

            }

            catch (RemoteException e)

            {

                handleInvokeException("Exception setting up SOAP call", e);

            }

        }

        else if (AeroflotSoapClientContainer.ARRIVAL_BY_AIRPORT_METHOD.equals(callable.getMethod()))

        {

            try

            {

                return Arrays.asList(new FlightStatusLocator().getFlightStatusSoap12().arrival(

                        (String) call.getParameters()[0],

                        DateFormat.getDateInstance(DateFormat.SHORT).format((Date) call.getParameters()[1]),

                        "airport",

                        "asc"));

            }

            catch (ServiceException e)

            {

                handleInvokeException("Exception setting up SOAP call", e);

            }

            catch (RemoteException e)

            {

                handleInvokeException("Exception setting up SOAP call", e);

            }

        }



        throw new ECFException("invalid method");

    }

}

 


3. Класс-наследник от AbstractSoapClientContainer, который реализует конкретный контейнер, создаваемый в бандле-клиенте, и через который будет осуществляться работа с сервисом IFlightStatusService. В конструкторе контейнера регистрируются вызовы - с помощью фабрики SoapCallableFactory создается матрица IRemoteCallable[][]. Каждая строка матрицы соответствует методам какого-либо интерфейса. Необходимо понимать, что имена методов - это имена методов интерфейса сервиса (например, IFlightStatusService), а не имена методов, определенных в WSDL. Код контейнера может быть таким:

package name.samolisov.ecf.webservices.demo;



import org.eclipse.ecf.remoteservice.IRemoteService;

import org.eclipse.ecf.remoteservice.client.IRemoteCallable;

import org.eclipse.ecf.remoteservice.client.RemoteServiceClientRegistration;

import org.eclipse.ecf.remoteservice.soap.client.AbstractSoapClientContainer;

import org.eclipse.ecf.remoteservice.soap.client.SoapCallableFactory;

import org.eclipse.ecf.remoteservice.soap.identity.SoapID;



public class AeroflotSoapClientContainer extends AbstractSoapClientContainer

{

    public static final String AIRPORT_LIST_METHOD = "getAirPortList";



    public static final String AIRPORT_INFO_METHOD = "getAirPort";



    public static final String ARRIVAL_BY_AIRPORT_METHOD = "getArrivalOrderByAirPort";



    public AeroflotSoapClientContainer(SoapID containerID)

    {

        super(containerID);



        // Create a callable that has the single 'AirportList' method

        IRemoteCallable[][] callables = new IRemoteCallable[][] {

                { SoapCallableFactory.createCallable(AIRPORT_LIST_METHOD),

                  SoapCallableFactory.createCallable(AIRPORT_INFO_METHOD),

                  SoapCallableFactory.createCallable(ARRIVAL_BY_AIRPORT_METHOD)}

        };



        // Register it

        registerCallables(new String[] { IFlightStatusService.class.getName() }, callables, null);

    }



    @Override

    protected IRemoteService createRemoteService(RemoteServiceClientRegistration registration)

    {

        // Return our service

        return new AeroflotSoapClientService(this, registration);

    }

}


4. Класс-наследник от BaseContainerInstantiator, который инкапсулирует логику создания контейнера. Код такого инстантиатора может быть следующим:

package name.samolisov.ecf.webservices.demo;



import org.eclipse.ecf.core.ContainerCreateException;

import org.eclipse.ecf.core.ContainerTypeDescription;

import org.eclipse.ecf.core.IContainer;

import org.eclipse.ecf.core.identity.IDFactory;

import org.eclipse.ecf.core.provider.BaseContainerInstantiator;

import org.eclipse.ecf.remoteservice.soap.identity.SoapID;

import org.eclipse.ecf.remoteservice.soap.identity.SoapNamespace;



public class AeroflotSoapClientContainerInstantiator extends BaseContainerInstantiator

{

    private static final String URL = "http://webservices.aeroflot.ru/flightstatus.asmx";



    @Override

    public IContainer createInstance(ContainerTypeDescription description, Object[] parameters)

            throws ContainerCreateException

    {

        try

        {

            SoapID soapID = null;

            if (parameters != null && parameters[0] instanceof SoapID)

                soapID = (SoapID) parameters[0];

            else if (parameters == null || parameters.length == 0)

                soapID = (SoapID) IDFactory.getDefault().createID(SoapNamespace.NAME, URL);

            else

                soapID = (SoapID) IDFactory.getDefault().createID(SoapNamespace.NAME, parameters);

            return new AeroflotSoapClientContainer(soapID);

        }

        catch (Exception e)

        {

            throw new ContainerCreateException("Could not create SoapClientContainer", e);

        }

    }



    @Override

    public String[] getSupportedAdapterTypes(ContainerTypeDescription description)

    {

        return getInterfacesAndAdaptersForClass(AeroflotSoapClientContainer.class);

    }

   

    @Override

    public Class<?>[][] getSupportedParameterTypes(ContainerTypeDescription description)

    {

        SoapNamespace soapNamespace = (SoapNamespace) IDFactory.getDefault().getNamespaceByName(SoapNamespace.NAME);

        return soapNamespace.getSupportedParameterTypes();

    }

}

 


Нужно не забыть зарегистрировать инстантиатор и связать его с названием типа контейнера. Для этого в файле plugin.xml необходимо определить расширение для точки org.eclipse.ecf.containerFactory:

<?xml version="1.0" encoding="UTF-8"?>

<?eclipse version="3.4"?>

<plugin>

   <extension point="org.eclipse.ecf.containerFactory">

      <containerFactory

           class="name.samolisov.ecf.webservices.demo.AeroflotSoapClientContainerInstantiator"

           name="ecf.aeroflot.soap.client">

      </containerFactory>

   </extension>

</plugin>

 


Собственно, этого достаточно для примера. Теперь, если нужно использовать веб-сервис аэрофлота в каком-либо бандле, то достаточно создать там контейнер типа ecf.aeroflot.soap.client и получить из него экземпляр интерфейса IRemoteService для интерфейса IFlightStatusService, после чего его можно использовать для осуществления как синхронных, так и асинхронных вызовов.

Чтобы продемонстрировать работу клиента, создадим тестовый бандл name.samolisov.ecf.webservices.demo.test, в котором будет всего один класс - JUnit тест AeroflotTest, наследуемый от ECFAbstractTestCase. В методе setUp данного теста будем создавать контейнер и IRemoteServiceContainerAdapter, а в методе tearDown() - освобождать ресурсы. Общий код тестового класса следующий:

package name.samolisov.ecf.webservices.demo.test;



import java.util.Date;

import java.util.List;



import name.samolisov.ecf.webservices.demo.IFlightStatusService;



import org.eclipse.ecf.core.ContainerFactory;

import org.eclipse.ecf.core.IContainer;

import org.eclipse.ecf.core.identity.ID;

import org.eclipse.ecf.core.util.ECFException;

import org.eclipse.ecf.remoteservice.IRemoteService;

import org.eclipse.ecf.remoteservice.IRemoteServiceContainerAdapter;

import org.eclipse.ecf.remoteservice.IRemoteServiceReference;

import org.eclipse.ecf.tests.ECFAbstractTestCase;

import org.osgi.framework.InvalidSyntaxException;



import ru.aeroflot.www.Airport;

import ru.aeroflot.www.Flight;



public class AeroflotTest extends ECFAbstractTestCase

{

    private IContainer container;



    private IRemoteServiceContainerAdapter containerAdapter;



    @Override

    protected void setUp() throws Exception

    {

        super.setUp();

        container = ContainerFactory.getDefault().createContainer("ecf.aeroflot.soap.client");

        containerAdapter = (IRemoteServiceContainerAdapter) container.getAdapter(IRemoteServiceContainerAdapter.class);

    }



    @Override

    protected void tearDown() throws Exception

    {

        super.tearDown();

        containerAdapter = null;

        container.dispose();

        container = null;

    }



    private IFlightStatusService getServiceProxy() throws InvalidSyntaxException, ECFException

    {

        IRemoteServiceReference[] refs = containerAdapter.getRemoteServiceReferences((ID) null,

                IFlightStatusService.class.getName(), null);



        assertNotNull(refs);

        assertTrue(refs.length > 0);



        IRemoteService remoteService = containerAdapter.getRemoteService(refs[0]);



        return (IFlightStatusService) remoteService.getProxy();

    }



    public void testGetAirPortList() throws Exception

    {

        IFlightStatusService proxy = getServiceProxy();

        assertNotNull(proxy);



        // Now call it

        List<Airport> airportList = proxy.getAirPortList();

        assertNotNull(airportList);



        for (Airport airport : airportList)

            System.out.println(airport.getCode());

    }



    public void testGetAirPortInfo() throws Exception

    {

        IFlightStatusService proxy = getServiceProxy();

        assertNotNull(proxy);



        Airport airport = proxy.getAirPort("SVX");

        assertNotNull(airport);



        System.out.println(airport.getName());

    }



    public void testGetArrivalOrderByAirPort() throws Exception

    {

        IFlightStatusService proxy = getServiceProxy();

        assertNotNull(proxy);



        List<Flight> flightes = proxy.getArrivalOrderByAirPort("SVX", new Date());

        assertNotNull(flightes);



        for (Flight flight : flightes)

            System.out.println(flight.getCompany());

    }

}

 


Запускать тестовый класс необходимо из пункта меню Run As -> JUnit Plug-In Test. Стоит проверить, что в конфигурацию запуска попали бандлы org.eclipse.ecf.remoteservice, org.eclipse.ecf.remoteservice.soap, org.eclipse.ecf.provider.remoteservice и org.eclipse.equinox.registry, причем у последнего параметр Auto-Start должен быть выставлен в true (данный бандл осуществляет формирование реестра точек расширения и без него новый тип контейнера - ecf.aeroflot.soap.client не будет зарегистрирован).

Надеюсь, что данная статья послужит полезным How-To по осуществлению взаимодействия ECF с SOAP веб-сервисами. К счастью, бандл, обеспечивающий взаимодействие по данному протоколу активно поддерживается и дорабатывается, поэтому, возможно, что скоро процедура создания своего клиента упростится. Сейчас так же появилась идея разработать бандл, обеспечивающий поддержку XML-RPC-протокола, возможно этим займется ваш покорный слуга, если на то будет время и желание.

Как всегда вы можете задавать мне вопросы, как по использованию SOAP, так и просто по использованию ECF в целом.

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

Комментариев нет:

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!