В версии 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 манифеста бандла:
- Bundle-ClassPath: lib/axis.jar,
- lib/commons-discovery-0.2.jar,
- lib/commons-logging-1.0.4.jar,
- lib/jaxrpc.jar,
- lib/saaj.jar,
- lib/wsdl4j-1.5.1.jar,
- .
Теперь нужно сгенерировать 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);
}
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");
}
}
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);
}
}
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();
}
}
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>
<?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());
}
}
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
Комментариев нет:
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!