Статьи и публикации

Интеграция SAP NetWeaver Portal 7.3 и MS Exchange 2010/2013

Антонов Д.А.

Антонов Д.А.

Руководитель отдела разработки Департамента практики SAP ООО «ЭнергоДата»

История вопроса

Одним из требований, возникающих при внедрении корпоративных порталов, является их тесная интеграция с уже развернутой структурой организации – электронной почтой, персональными средствами планирования и управления рабочим графиком. В первую очередь это наиболее распространенные системы – Microsoft Exchange и Lotus Notes. Пользовательские функции взаимодействия с персональной почтой и календарем поставляются в составе рабочей среды сотрудничества (пакет Collaboration) в виде набора готовых iView и страниц. Для обеспечения универсальности разработанных визуальных интерфейсов в архитектуру NetWeaver был добавлен дополнительный транспортный слой. То есть взаимодействие с конечными интерфейсами корпоративных почтовых систем происходит через выделенный программный объект, преобразующий вызовы набора стандартных методов библиотеки SAP NetWeaver в специфичные протоколы и стандарты, поддерживаемые целевыми системами.

Для интеграции SAP Portal с MS Exchange существовали следующие возможности:

  • Взаимодействие с интерфейсом Outlook Web Access (OWA)
  • Использование CDO (Collabarative Data Objects);
  • Использование протокола WebDAV.

Первый способ исторически появился еще в SAP Portal 6.0 и не использует каких-либо транспортных слоев. Однако его использование сопряжено с рядом неудобств (например, сложностями сквозной авторизации) и фактически является внедрением интерфейса OWA внутрь портала. Для более полной интеграции и контроля были разработаны новые средства, указанные выше – интеграция посредством CDO и WebDAV, реализованная через транспортный слой. Наиболее часто из двух указанных применялась интеграция через WebDAV, поскольку она требовала меньшего количества действий по настройке и конфигурированию на стороне Exchange.

Однако, начиная с версии MS Exchange 2010, компания Microsoft объявила об отказе от поддержки этих средств коммуникации, введя единый интерфейсный стандарт на основе веб-сервисов – EWS (Exchange Web Services). В результате интеграция стандартными средствами SAP Portal с версиями MS Exchange 2010/2013 стала невозможна. Наша проектная команда столкнулась именно с такой ситуацией, потребовавшей разработки собственного интерфейса интеграции с использованием EWS. Далее будут описаны основные шаги по реализации собственного транспортного объекта.

Реализация объекта транспорта

Имеющиеся способы подключения портала (CDO, WebDAV) использовали механизм транспорта. Это определило техническую архитектуру – объект транспорта и формат файла – .sda. Дополнительное внутреннее требование – использование DC и NWDI.

Для работы с Exchange используется код из внешнего проекта «EWS JAVA API» (http://archive.msdn.microsoft.com/ewsjavaapi). На момент написания статьи проект имел версию 1.2. Для работы этого проекта необходимы дополнительные файлы, о чем прямо говорится в файле «Compiling the EWS Java API.RTF». Мы использовали commons-codec-1.8.jar, commons-httpclient-3.1.jar, commons-logging-1.1.3.jar, jcifs-1.3.17.jar, поместив их в DC типа «library». Основной проект использовал public part типа «compilation» и «assembly». В итоге эти файлы вошли в состав конечного sda-пакета.

Описание вспомогательного сервиса

Для работы транспорта нужно, чтобы .jar с ним был постоянно загружен в память и таким образом являлся доступным загрузчику классов. Это обеспечивается запуском вспомогательного портального сервиса. Вот как выглядит его описание в portalapp.xml:

<service name="ExchangeTransportService">
  <service-config>
    <property name="className" value="ru.energodata.ExchangeTransportService"/>
    <property name="classNameFactory" value=""/>
    <property name="classNameManager" value=""/>
    <property name="poolFactory" value="0"/>
    <property name="startup" value="true"/>
  </service-config>
  <service-profile>
    <property name="generic_service_key" value="ru.energodata.ExchangeTransportService"/>
    <property name="generic_classloader_registration" value="yes"/>
    <property name="generic_so_registration" value="no"/>
    <property name="proxy" value=""/>
  </service-profile>
</service>

Ключевыми являются следующие моменты:

Сервис стартует автоматически. Ручной запуск не работает. Это неудобно для отладки, но вызвано архитектурой установки сервиса, о чем будет идти речь ниже, в описании файла конфигурации.

Свойство «generic_classloader_registration» установлено в «yes», что позволяет избежать написания кода регистрации, воспользовавшись уже готовым функционалом. Подробнее об этом также далее по тексту.

Сам транспорт – это класс, созданный соответствующим мастером. Вот как выглядит интерфейсная часть:

package ru.energodata;
import com.sapportals.portal.prt.service.IService;
public interface IExchangeTransportService extends IService
{
    public static final String KEY = "ru.energodata~exchtransp.ExchangeTransportService";
    public String getProxy();
}

Вот так выглядит реализация:

package ru.energodata;
import com.sap.ip.collaboration.core.api.fwk.portal.GenericService;
import com.sapportals.portal.prt.service.IServiceContext;
public class ExchangeTransportService extends GenericService implements IExchangeTransportService
{
      // сервис нужен для регистрации ClassLoader-а
      // регистрация происходит в super, т.к. включен параметр
//"generic_classloader_registration"
      private IServiceContext _context;
      @Override
      public String getProxy()
      {
            return _context.getServiceProfile().getProperty("proxy");
      }
      @Override
      public void init(IServiceContext ctxt)
      {
            _context = ctxt;
            super.init(ctxt);
      }
}

Важный момент – класс наследуется от GenericService. Именно в нем находится код регистрации, который вкупе со свойством «generic_classloader_registration» позволяет не писать код этот код у себя.

Свойство «proxy» и возвращающий его метод «getProxy» необходим, если выход на веб-сервис Exchange идет через прокси-сервер. В нашем случае подключение прямое, поэтому свойство пустое.

Описание программного интерфейса транспорта

Портал при запуске создает экземпляр класса средствами reflection. Транспорт – класс, реализующий ряд интерфейсов и унаследованный от класса «AbstractTransport»:

package ru.energodata;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import com.sap.ip.collaboration.gw.api.enums.GroupwareItemType;
import com.sap.ip.collaboration.gw.api.enums.TransportType;
import com.sap.ip.collaboration.gw.api.framework.groupware.AbstractTransport;
import com.sap.ip.collaboration.gw.api.framework.groupware.GroupwareException;
import com.sap.ip.collaboration.gw.api.framework.groupware.IAttachment;
import com.sap.ip.collaboration.gw.api.framework.groupware.ICalendarItem;
import com.sap.ip.collaboration.gw.api.framework.groupware.ICalendarReadTransport;
import com.sap.ip.collaboration.gw.api.framework.groupware.ICalendarSendTransport;
import com.sap.ip.collaboration.gw.api.framework.groupware.IDateRange;
import com.sap.ip.collaboration.gw.api.framework.groupware.IGWResourceManager;
import com.sap.ip.collaboration.gw.api.framework.groupware.IGroupwareCredentials;
import com.sap.ip.collaboration.gw.api.framework.groupware.IGroupwareItem;
import com.sap.ip.collaboration.gw.api.framework.groupware.ISupportability;
@SuppressWarnings("unchecked")
public class ExchangeCalendarTransport extends AbstractTransport implements ICalendarReadTransport, ICalendarSendTransport, ISupportability
{
      private static String transportId = "ru.energodata.ExchangeCalendarTransport";
      private IGWResourceManager _manager;
      private List _configurations;
      private String _alias = "TransportErrorSeeLog";
      @Override
      public String getServerAlias()
      {
            return _alias;
      }
      @Override
      public GroupwareItemType getSupportedItemTypeEnum()
      {
            return GroupwareItemType.CALENDAR;
      }
      @Override
      public String getTransportDescription(Locale locale)
      {
            return "EWS Microsoft Exchange";
      }
      @Override
      public String getTransportName()
      {
            return transportId;
      }
      @Override
      public TransportType getTransportTypeEnum()
      {
            return TransportType.BOTH;
      }
      @Override
      public void initialize(IGWResourceManager manager, List configurations) throws GroupwareException
      {
            _manager = manager;
            _configurations = configurations;
            _alias = ExchangeCore.getAlias(configurations);
      }
      @Override
      public void terminate() throws GroupwareException
      {
      }
      @Override
      public Map getAvailabilityInfo(IDateRange range, int timeInterval, List addresses, IGroupwareCredentials credential) throws GroupwareException
      {
            ExchangeCore core = null;
            try
            {
                  HashMap res = new HashMap();
                  for (int i = 0; i < addresses.size(); i++)
                  {
                        String address = (String)addresses.get(i);
                        core = new ExchangeCore(_manager, _configurations, credential, address);
                        res.put(address, core.getAvailabilityInfo(range, timeInterval));
                  }
                  return res;
            }
            catch (Exception e)
            {
                  throw Exceptions.processing(e, core);
            }
      }
      @Override
      public IAttachment getAttachmentContent(String itemId, String attachmentId, IGroupwareCredentials credential) throws GroupwareException
      {
            throw new UnsupportedOperationException("Method getAttachmentContent() not yet implemented.");
      }
      @Override
      public String getContent(String arg0, IGroupwareCredentials arg1) throws GroupwareException
      {
            throw new UnsupportedOperationException("Method getContent() not yet implemented.");
      }
      @Override
      public IGroupwareItem getItem(String itemId, IGroupwareCredentials credential) throws GroupwareException
      {
            ExchangeCore core = null;
            try
            {
                  core = new ExchangeCore(_manager, _configurations, credential);
                  IGroupwareItem res = core.getItem(itemId);
                  if (res != null)
                        return res;
                  throw new Exception("getItem вернул null");
            }
            catch (Exception e)
            {
                  throw Exceptions.processing(e, core);
            }
      }
      @Override
      public List getItemList(IDateRange range, IGroupwareCredentials credential) throws GroupwareException
      {
            ExchangeCore core = null;
            try
            {
                  core = new ExchangeCore(_manager, _configurations, credential);
                  return core.getItemList(range);
            }
            catch (Exception e)
            {
                  throw Exceptions.processing(e, core);
            }
      }
      @Override
      public List getItemList(IDateRange range, Properties searchCriteria, IGroupwareCredentials credential, int nCount) throws GroupwareException
      {
            return new ArrayList();
      }
      @Override
      public GroupwareItemType getSupportedItemType()
      {
            return getSupportedItemTypeEnum();
      }
      @Override
      public TransportType getTransportType()
      {
            return getTransportTypeEnum();
      }
      @Override
      public void remove(String id, IGroupwareCredentials credential, boolean isSeries) throws GroupwareException
      {
      }
      @Override
      public void remove(String id, IGroupwareCredentials credential) throws GroupwareException
      {
      }
      @Override
      public String save(IGroupwareItem item, IGroupwareCredentials credential) throws GroupwareException
      {
            return "";
      }
      @Override
      public String send(IGroupwareItem item, IGroupwareCredentials credential) throws GroupwareException
      {
            ExchangeCore core = null;
            try
            {
                  core = new ExchangeCore(_manager, _configurations, credential);
                  if (item instanceof ICalendarItem)
                        core.send ((ICalendarItem) item);
                  else
                        throw new Exception("item не ICalendarItem");
            }
            catch (Exception e)
            {
                  throw Exceptions.processing(e, core);
            }
            return "";
      }
      @Override
      public Map getSupp_Heartbeat(IGroupwareCredentials credential, Properties congfigProperty)
      {
            return new HashMap();
      }
      @Override
      public List getSupp_TransportConfigs()
      {
            return _configurations;
      }
}

Несколько комментариев по приведенному коду:

Первым вызываемым методом является метод «initialize». Он обеспечивает инициализацию (начальные значения) полей транспорта. Собственно, поэтому значение псевдонима по умолчанию «TransportErrorSeeLog». В нашей работе не было ни одного случая, чтобы метод «initialize» не вызвался. Возможно, значение по умолчанию вообще можно не определять.

Список конфигураций приходит в метод «initialize» в поле «configurations». В нашем случае в списке всегда находилось только одно значение. Код выбора конфигурации выглядит так:

 private static class Configuration
      {
            public final String Server;
            public final String Protocol;
            public final String SystemAlias;
            private Configuration(String server, String protocol, String systemAlias)
            {
                  Server = server;
                  Protocol = protocol;
                  SystemAlias = systemAlias;
            }
            public static Configuration get (List configurations) throws Exception
            {
                  // взять первую конфигурацию 
                  for (Iterator e = configurations.iterator(); e.hasNext();)
                  {
                        Properties prop = (Properties) e.next();
                        String configServer = (String) prop.get("exchangeserver");
                        String systemAlias = (String) prop.get("aliasname");
                        if (systemAlias == null)
                              throw new Exception("Не указана система в настройках транспорта");
                        String protocol = (String) prop.get("protocol");
                        if (protocol == null)
                              throw new Exception("Не указан протокол в настройках транспорта");
                        return new Configuration(configServer, protocol, systemAlias);
                  }
                  throw new Exception("Нет ни одной конфигурации.");
            }
      }

Ключи, которым соответствуют значения («exchangeserver», «aliasname», «protocol»), определяются файлом конфигурации, который подробнее будет рассмотрен позже.

Но вернемся к коду транспорта. Метод «getAvailabilityInfo» вызывается порталом при просмотре запланированных встреч. Метод «send» вызывается порталом при назначении новой встречи.

Класс «ExchangeCore» не имеет какой-либо логики, он просто переводит вызовы портала в вызовы веб-сервиса Exchange, попутно проводя конвертацию типов портала в типы Exchange и обратно.

Среди подводных камней хотелось бы отметить совершенно неочевидный факт, что портал использует время в GMT, а Exchange – нет. При работе со временем приходится делать конвертации и, чтобы в этом не запутаться, понадобилось сделать пару вспомогательных классов:

private class AppointmentsEnumerator
      {
            private Iterator<Item> _iterator;
            private DateConvertor _c = new DateConvertor();
            public Appointment Current;
            public Date getStartGMT() throws ServiceLocalException
            {
                  return _c.toGMT(Current.getStart());
            }
            public Date getEndGMT() throws ServiceLocalException
            {
                  return _c.toGMT(Current.getEnd());
            }
            public AppointmentsEnumerator(Date startTime, Date endTime) throws Exception
            {
                  CalendarFolder calendarFolder = (CalendarFolder) Folder.bind(_service, WellKnownFolderName.Calendar);
                  // даты сдвигаем обратно, т.к. MS ожидает получить данные не в GMT
                  _iterator = calendarFolder.findAppointments(new CalendarView(_c.backGMT(startTime), _c.backGMT(endTime))).iterator();
            }
            public boolean next()
            {
                  if (_iterator == null || !_iterator.hasNext())
                        return false;
                  Current = (Appointment) _iterator.next();
                  return true;
            }
      }
public class DateConvertor
{
      private TimeZone z = TimeZone.getDefault();
      private GregorianCalendar c = new GregorianCalendar(z);
      public Date toGMT (Date d)
      {
            c.setTimeInMillis(d.getTime() + z.getRawOffset());
            return c.getTime();
      }
      public Date backGMT (Date d)
      {
            c.setTimeInMillis(d.getTime() - z.getRawOffset());
            return c.getTime();
      }
}

Сборка пакета установки

Пакет установки собирается штатными средствами, получается стандартный файл .sda. Этот файл нельзя сразу разворачивать на портале, поскольку в него надо добавить файл конфигурации. Файл конфигурации мы добавляли вручную через менеджер файлов FAR. Да, подобный подход делает невозможным использование полностью автоматической сборки в NWDI. Эта проблема осталась в нашем случае нерешенной.

Файл конфигурации и его добавление в пакет установки

Транспорт – это configurable object, который конфигурируется файлами конфигурации. Нам не удалось получить готовый файл конфигурации с помощью мастеров NWDS, поэтому файл пришлось делать вручную. Далее описывается файл конфигурации «с конца», т.е. начиная с файла sda.

В корне sda находится файл «exchtransp.configarchive». Это архив zip, содержащий пустую директорию «installmetaexpanded» и два файла:

«installmetalibexchtransp.configmeta» и «META-INFMANIFEST.MF».

Вот как выглядит MANIFEST.MF:
Manifest-Version: 1.0
CA-Version: 6.0.1.1
CA-Creation-User: unknown
CA-Dependencies: bc.util.prjconfig, bc.sf.prjconfig, bc.sf.service.prj
 config, bc.rf.prjconfig
CA-Name: exchtransp.prjconfig
CA-Creation-Date: 20130904
CA-Creation-Machine: unknown
CA-Creation-Time: 1648

Файл exchtransp.configmeta – это снова архив zip, содержащий в свою очередь файлы «META-INFMANIFEST.MF», 
«collaborationtransportsEWSXchgTransport.cc.xml», «collaborationtransportsbundlesplugin.properties» 
и «collaborationtransportsbundlesclassesEWSXchgTransport.properties».

Вот как выглядит MANIFEST.MF:
Manifest-Version: 1.0
CMA-Name: exchtransp
CMA-Version: 6.0.1.0
CMA-Storage: sfs
CMA-Creation-Time: 0419
CMA-Creation-Date: 20120905
CMA-Creation-User: undefined
CMA-Creation-Machine: undefined
CMA-Dependencies: coll.appl.gw.fwk

Файл «EWSXchgTransport.cc.xml» задает состав элементов configurable object, в нашем случае, он задает состав ключей параметров, пришедших в класс Configuration, описанный ранее. Выглядит файл так:

<ConfigClass name="EWSXchgTransport" extends="Transport" hotReload="true" hotLoad="true" hotUnload="true">
            <attribute name="name" type="string" default="EWSXchgTransport"/>
            <attribute name="aliasname" type="string" mandatory="true" default="DAVExchange"/>
            <attribute name="protocol" type="enum" values="http,https" mandatory="true" advanced="true" default="https"/>
            <attribute name="exchangeserver" type="string" mandatory="true"/>
            <attribute name="transportClassNames" type="string" constant="ru.energodata.ExchangeCalendarTransport" visibleField="false" visibleColumn="false"/>
</ConfigClass>

Файл «plugin.properties» служебный и возможно, даже необязательный. В нашем случае он выглядел так:

listdesc=Transports for different groupware services like mail, calendar, task etc.
maindesc=Transports for different groupware services like mail, calendar, task etc.
lbl=Transports

Файл «EWSXchgTransport.properties» задает текстовые описания свойств. Эти описания портал использует при отображении диалога конфигурации транспорта. Выглядит файл так::

lbl.transportClassNames=Transport class names
tip.transportClassNames=Name of the transport classes
listdesc=Transport for connecting to Microsoft Exchange Server using EWS
maindesc=Transport for connecting to Microsoft Exchange Server using EWS
lbl.aliasname=System alias
tip.aliasname=Alias defined for the Exchange server in the system configuration
lbl.name=Name
tip.name=Name of the transport
lbl.exchangeserver=Exchange server
tip.exchangeserver=The name of the exchange server
lbl=EWSXchg Transport
tip=Transport for connecting to Microsoft Exchange Server using EWS
lbl.protocol=Protocol
tip.protocol=Select the protocol to connect to the exchange server

Как видно, он содержит пары ключ–значение. Ключ состоит из метки (lbl) либо всплывающей подсказки (tip) и названия свойства из файла «EWSXchgTransport.cc.xml».

Установка и настройка объекта транспорта

Развертывание пакета

После создания sda и добавления в него файла конфигурации пакет разворачивается любым штатным способом развертывания. После разворачивания пакета портал необходимо перезагрузить. После перезагрузки транспорт необходимо сконфигурировать.

Конфигурирование транспорта

Конфигурирование происходит в System Administration | System Configuration | Collaboration:

схема работы решения на примере процесса согласования заявки на платеж

Если транспорт написан правильно, то он появится в списке Topics, а справа появится его текстовое описание из файла «EWSXchgTransport.properties».

Заходим в выбранный транспорт и создаем экземпляр транспорта кнопкой «New». После этого входим в редактирование и проставляем параметры:

схема работы решения на примере процесса согласования заявки на платеж

Как видно из иллюстрации, свойства разных типов – список/строка, и разной доступности – стандартные/расширенные. Все эти характеристики задаются в файле «EWSXchgTransport.cc.xml». Текст, под которым появится свойство, задается в файле «EWSXchgTransport.properties».

Итак, в редактировании указывается протокол и адрес. Из этих данных сформируется URL, по которому будет выполняться обращение к веб-сервису Exchange.

Кроме того, здесь задается псевдоним системы. Псевдоним используется далее при настройке репозитория календаря.

Настройка репозитория календаря

Календарь в портале представлен как репозиторий KM. Настройка репозитория происходит в System Administration | System Configuration | Knowledge Management | Content Management | Repository Managers | Calendar Repository:

схема работы решения на примере процесса согласования заявки на платеж

Заходим в репозиторий календаря и создаем экземпляр репозитория кнопкой «New». После этого входим в редактирование и проставляем параметры созданного репозитория:

схема работы решения на примере процесса согласования заявки на платеж

Основное, что необходимо сделать – выбрать транспорт из списка предлагаемых. Список представлен псевдонимами всех настроенных на портале экземпляров транспорта. Как видно из иллюстрации, в нашем случае имеются три транспорта – один DAV и два EWS. Чтобы не усложнять себе жизнь дополнительным кодом выбора транспорта, мы всегда устанавливали один транспорт для календаря. После выбора транспорта порталу понятно, какой транспорт использовать для подключения к Exchange. Однако это не вся настройка. Теперь надо решить вопрос с авторизацией при обращении к Exchange.

Определение логина/пароля для доступа к Exchange

В качестве логина/пароля для входа на EWS берется mapped user / mapped password для системы, псевдоним которой был задан в настройках транспорта. В нашем случае псевдоним – «DAVExchange».

Тип системы, скорее всего, не имеет значения, поскольку сама система не используется, а используется лишь псевдоним и пара логина/пароля для этой системы (mapped user / password). В нашем случае система была мигрирована из предыдущей версии портала и имела тип «Not defined».

Итак, есть система с псевдонимом «DAVExchange» и у пользователей настроены соответствия user / password для этой системы. Осталось последнее – настройка разрешений.

Выдача разрешений

Функционал Collaboration портала позволяет увидеть доступность (т.е. график встреч) не только из своего календаря, но и из календаря другого пользователя. Технически чтобы увидеть доступность другого пользователя, надо выполнить обращение к веб-сервису от имени этого «другого пользователя». Для этого сервису необходимо прочитать mapped user / password этого пользователя. Такая операция по умолчанию запрещена.

Чтобы сервис, вызываемый из сессии пользователя, мог прочитать чужой mapped user / password, ему необходимо предоставить право на действие «Read_User_Mapping_Credentials». В нашем случае действие было добавлено в роль, в назначенную пользователям.

Тестирование работоспособности

Для тестирования надо зайти в функционал Collaboration, для чего кликнуть пункт Collaboration на главной странице портала:

схема работы решения на примере процесса согласования заявки на платеж

Откроется новое окно Collaboration, в котором надо выбрать пункт меню «Show Availability»:

схема работы решения на примере процесса согласования заявки на платеж

Откроется новое окно, представляющее собой стандартное iView из поставки SAP, в котором будут отображаться элементы календаря, полученные из MS Exchange по EWS. Если в процессе вызова возникли какие-либо ошибки (например, вследствие недоступности сервера MS Exchange или неверных логина/пароля для доступа к почтовой записи), то в диалоговом окне отобразится сообщение об ошибке. Однако перечень этих сообщений определяется классами, поставляемыми SAP, и довольно узок. Поэтому для точной диагностики лучше использовать средства просмотра системных логов.

Заключение

В результате последовательно выполненных разработок и настроек проектной команде удалось достичь поставленной цели, проведя интеграцию в требуемом техническим заданием объеме. Разумеется, наша реализация не покрывает все возможные варианты использования. Кроме того, выбранная архитектура имеет несколько потенциальных недостатков – например, если несколько учетных записей корпоративного портала имеют одинаковый адрес корпоративной электронной почты, но в автоматизируемой компании такая ситуация является ненормальной и должна решаться административно.

Мы надеемся, что описанный опыт поможет другим проектным командам быстрее получить результат, критически используя и совершенствуя предложенное нами решение.

Источник: http://sapland.ru/articles/stats/integratsiya-sap-netweaver-portal-7-3-i-ms-exchange-20102013.html