Формат EnterpriseData
20.10.2017

Обмен данными с конфигурацией через веб-сервисы

Внимание! Данный функционал доступен в "Библиотеке стандартных подсистем", начиная с версии 2.3.1.62.

Для обмена данными через формат EnterpriseData у конфигураций, использующих "Библиотеку стандартных подсистем", есть два веб-сервиса:

Т.к. объем передаваемых через веб-сервисы данных ограничен, данные передаются в виде архивов формата ZIP. Если размер архива слишком велик для передачи через веб-сервис – его разбивают на части и передают по частям. Максимальный размер передаваемого через веб-сервис за сообщения зависит от ряда факторов (от пропускной способности сети, от настроек веб-сервера, от объема свободной памяти на передающей и приемной стороне и т.д.). Опыт показывает, что лучше не передавать через веб-сервис данные размером более нескольких мегабайт.

Собственно задача обмена данными включает в себя две подзадачи:

Особенности работы методов веб-сервисов

Большинство методов обоих веб-сервисов имеют выходной параметр – строку ErrorMessage. В случае если внутри конфигурации произошла ошибка, связанная с бизнес-логикой – в эту строку будет записана информация об этой ошибке. Если ошибок в процессе работы метода не было – в строку ErrorMessage будет помещена пустая строка. Если же в процессе работы метода возникла системная ошибка (например, на стороне конфигурации не удалось разархивировать полученный архив) – веб-метод  сгенерирует исключение (exception).

Большинство методов  обоих веб-сервисов возвращают строки, но в текущей версии возвращаемые строки всегда пустые (кроме EnterpriseDataUpload.PutDataActionResult – он возвращает статус обработки данных на стороне конфигурации – “Active”, “Completed” либо “Failed”).

Ниже на примерах мы рассмотрим, как использовать эти веб-сервисы для обмена данными с конфигурациями из языков C# и Java.

Что нужно для работы

На стороне конфигурации

На стороне конфигурации должны быть развернуты веб-сервисы EnterpriseDataUpload и EnterpriseDataExchange соответствующих версий (в данном случае была использована версия 1.0.1.1). При открытии этих двух URL-адресов в браузере (нужно подставить правильное для вашей инсталляции «1С:Предприятия» имя веб-сервера и публикации):

http://<веб-сервер>/<веб-приложение>/ws/EnterpriseDataExchange_1_0_1_1?wsdl

http://<веб-сервер>/<веб-приложение>/ws/EnterpriseDataUpload_1_0_1_1?wsdl

должны выводиться WSDL-описания сервисов:

C#

В примерах использовалась Visual Studio 2012. В ней было создано консольное приложение на C#, в него импортированы веб-сервисы:

Java

Использовалась среда разработки Eclipse 4.4.2. Для генерации кода по WSDL файлов веб-сервисов применялась утилита wsdl2java из фреймворка Apache CXF 2.7.16.

Простой обмен данными с конфигурациями с помощью формата EnterpriseData

Формат сообщения

Объекты, предназначенные к обмену, «упакованы» в сообщение (Message), структуру в формате XML. Корневой элемент сообщения называется Message и содержит два дочерних элемента:

Объект может содержать в себе ссылки на другие объекты (например, документ «Акт выполненных работ» может содержать в себе одну или несколько ссылок на номенклатуру). В этом случае, если мы импортируем данные в информационную базу, все объекты, на которые мы ссылаемся из «родительского» объекта, должны либо уже существовать в системе, либо их описание должно содержаться в том же XML файле.

Если нам необходимо удалить какой-то объект, в коллекцию Body надо добавить элемент типа «УдалениеОбъекта», и в этом элементе сослаться на удаляемый объект (см. описание типа «УдалениеОбъекта» в схеме EnterpriseData_X_Y_Z.xsd).

С помощью формата EnterpriseData нам доступны операции создания, обновления и удаления объектов. На данный момент для корректной загрузки данных в типовые решения все объекты должны содержать заполненный элемент «Ссылка» из элемента «Ключевые свойства» (GUID в форме строки). Это первичный ключ объекта. Конфигурации ведут себя следующим образом:

Пример сообщения

Копировать в буфер обмена
<Message xmlns:msg="http://www.1c.ru/SSL/Exchange/Message" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<msg:Header>    <msg:Format>http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.0</msg:Format>
<msg:CreationDate>2015-04-09T11:57:48</msg:CreationDate>
<msg:AvailableVersion>1.0</msg:AvailableVersion> </msg:Header> <Body xmlns="http://v8.1c.ru/edi/edi_stnd/EnterpriseData/1.0">                <Справочник.Контрагенты> <КлючевыеСвойства> <Ссылка>15ee9feb-696e-11e4-a6fc-5404a67fc69d</Ссылка> <НаименованиеПолное>Леннон Джон</НаименованиеПолное> <ЮридическоеФизическоеЛицо>ЮридическоеЛицо</ЮридическоеФизическоеЛицо> </КлючевыеСвойства>                        <Наименование>Леннон Джон</Наименование> <ОбособленноеПодразделение>false</ОбособленноеПодразделение> </Справочник.Контрагенты> <УдалениеОбъекта> <СсылкаНаОбъект> <СсылкаНаОбъект> <КонтрагентыГруппаСсылка>15ee9feb-696e-11e4-a6fc-5404a67fc69d</КонтрагентыГруппаСсылка> </СсылкаНаОбъект>                        </СсылкаНаОбъект> </УдалениеОбъекта> </Body> </Message>

В секции <Header> - ссылка на формат сообщения, дата создания сообщения и версия формата.

В секции <Body> - описание нового контрагента (тип «Справочник.Контрагенты») с полным наименованием «Леннон Джон» и описание удаления объекта по ключу. Ключ в объекте  <УдалениеОбъекта> (элемент <СсылкаНаОбъект>) – тот же, что и у нового контрагента. В результате обработки этого сообщения конфигурация создаст нового контрагента и тут же удалит его. 

Веб-сервис EnterpriseDataUpload

EnterpriseDataUpload – интерфейс исключительно для импорта данных в формате EnterpriseData в конфигурацию из сторонних приложений. Условия задачи: у нас есть XML файл с данными в формате EnterpriseData, надо передать его в конфигурацию и убедиться в том, что на стороне конфигурации данные успешно получены.

Алгоритм работы следующий:

  1. Заархивировать XML-файл с данными. Если размер архива превышает некоторое предельное значение (например, 2Мб) – разбиваем его на части размером не более 2Мб каждая.
  2. Проверить соединение с помощью метода TestConnection.
  3. Отправить каждую часть архива с помощью метода PutFilePart. Если мы передаем архив одним файлом – второй параметр метода PutFilePart, PartNumber, должен быть равен нулю.  Если архив из нескольких частей – параметр PartNumber должен соответствовать номеру части, начиная с единицы.
  4. После отправки всех частей архива вызвать метод PutData.
  5. Получить результат выполнения метода PutData можно с помощью метода PutDataActionResult. Т.к. обработка полученных данных на стороне конфигурации занимает какое-то время, можно вызывать PutDataActionResult в цикле один раз в, например, 5 секунд, пока он не вернет строку «Completed» (или «Failed», если данные по какой-либо причине обработать не удалось, или «Canceled» если операция отменена).

Пример на C#

Итак, у нас есть архив с данными в формате EnterpriseData (один или несколько файлов). Не будем описывать, как в программе создать архив и разбить его на несколько файлов, чтобы сэкономить время – желающие смогут легко найти примеры подобного кода, используя поисковые сервера и соответствующие запросы.

Реализуем функцию, которая принимает такие входные параметры:

Путь до архивированного файла  с данными. Если архив умещается в одном файле – это имя файла с полным путем, включая расширение (например, “C:\Exchange\data.zip”). Если же архив разбит на несколько частей, то это будет имя любого из файлов без расширения и точки (например, “C:\Exchange\data”). Предполагается, что это последовательность файлов с расширениями “.001”, “.002” и т.д.

Реализация функции (в виде статического метода) в листинге ниже. Диагностическая информация выводится в консоль.

Копировать в буфер обмена
private static void PutData2WebService(string serviceURL, string user, string password, string fileName)
{
    EnterpriseDataUpload.EnterpriseDataUpload_1_0_1_1 edDataUpload = new EnterpriseDataUpload.EnterpriseDataUpload_1_0_1_1();
    edDataUpload.Url = serviceURL;
    edDataUpload.Credentials = new System.Net.NetworkCredential(user, password);
    string result = edDataUpload.Ping();
    System.Console.WriteLine("Invoking ping " + result);
    string errorMessage;
    bool b = edDataUpload.TestConnection(out errorMessage);
    if (b)
    {
        //если передано непосредственно имя ZIP-архива - читаем одним куском
        bool singleFile = fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase);
        string fileId = Guid.NewGuid().ToString();
        if (singleFile)
        {
            byte[] partData = System.IO.File.ReadAllBytes(fileName);
            //передаем 0 в качестве номера части файла
            result = edDataUpload.PutFilePart(fileId, 0, partData, out errorMessage);
            if (errorMessage != string.Empty)
            {
                System.Console.WriteLine("PutFilePart error: " + errorMessage);
                return;
            }
        }
        else
        {
            //счетчик частей файла начинается с единицы
            int partNumber = 1;
            //считаем, что нам передали имя последовательности файлов (*.001, *.002 и т.д.) БЕЗ расширения и точки на конце
            string filePath = fileName + "." + (partNumber).ToString("d3");
            while (System.IO.File.Exists(filePath))
            {
                System.Console.WriteLine("Reading from: " + filePath);
                byte[] partData = System.IO.File.ReadAllBytes(filePath);
                string putFilePartResult = edDataUpload.PutFilePart(fileId, partNumber, partData, out errorMessage);
                if (errorMessage != string.Empty)
                {
                    System.Console.WriteLine("PutFilePart error: " + errorMessage);
                    return;
                }
                filePath = fileName + "." + (++partNumber).ToString("d3");
            }
        }
        string operationId;
        result = edDataUpload.PutData(fileId, out operationId, out errorMessage);
        if (errorMessage != string.Empty)
        {
            System.Console.WriteLine("PutData error: " + errorMessage);
            return;
        }
        result = edDataUpload.PutDataActionResult(operationId, out errorMessage);
        if (errorMessage != string.Empty)
        {
            System.Console.WriteLine("PutDataActionResult error: " + errorMessage);
            return;
        }
        while (result == "Active")
        {
            //ждем в цикле 5 секунд - ожидаем окончания обработки данных на стороне конфигурации и спрашиваем снова
            System.Console.WriteLine("PutDataActionResult: " + result);
            System.Threading.Thread.Sleep(5000);
                   
            result = edDataUpload.PutDataActionResult(operationId, out errorMessage);
        }
        System.Console.WriteLine("PutDataActionResult: " + result);
        System.Console.WriteLine("PutData2WebService finished");
    }
    else
    {
        System.Console.WriteLine("TestConnection failed: "+ errorMessage);
    }
}


Вызов метода:

Копировать в буфер обмена
PutData2WebService("http://server/1c_app/ws/EnterpriseDataUpload_1_0_1_1", 
                "Administrator",
                "pass@word",
                @"c:\exchange\data.zip");
Пример на Java

Принципиально ничем не отличается от реализации на C#.

Копировать в буфер обмена
private static void putData2WebService(String serviceURL, String user, String password, String fileName)
{
 Authenticator.setDefault(new Authenticator() {
  @Override
  protected PasswordAuthentication getPasswordAuthentication() {
   return new PasswordAuthentication(user, password.toCharArray());
  }
 });
 
 URL url = null;
 
 try {
  url = new URL(serviceURL);
 } catch (MalformedURLException e) {
  e.printStackTrace();
 }
 
 EnterpriseDataUpload1011 ss;
 ss = new EnterpriseDataUpload1011(url,
    EnterpriseDataUpload1011.SERVICE);
 
 EnterpriseDataUpload1011PortType port = ss.getEnterpriseDataUpload1011Soap12();
 System.out.println("Invoking ping...");
 port.ping();
 System.out.println("Ping Ok");
 
 Holder<String> outString = new Holder<String>();
 Boolean testConnectionOk =  port.testConnection(outString);
 
 if (testConnectionOk)
 {
  //если передано непосредственно имя ZIP-архива - читаем одним куском
  Boolean singleFile = fileName.toLowerCase().endsWith(".zip");
  String fileId = UUID.randomUUID().toString();
  String strResult;
  
  if (singleFile)
  {
   Path path = Paths.get(fileName);
   byte[] data;
   
   try
   {
    data = Files.readAllBytes(path);
   }
   catch (Exception e)
   {
    System.err.println("Exception when open file " + path);
    return;
   }
   
   //передаем 0 в качестве номера части файла
   strResult = port.putFilePart(fileId, 0, data, outString);
 
   if (!outString.value.isEmpty())
              {
    System.err.println("PutFilePart error: " + outString);
    return;
              }
  }
  else
  {
   //счетчик частей файла начинается с единицы
   int partNumber = 1;
   
   //считаем, что нам передали имя последовательности файлов (*.001, *.002 и т.д.) БЕЗ расширения и точки на конце
   Path filePath = Paths.get(fileName + "." + String.format("%03d", partNumber));
   
   while (Files.exists(filePath))
   {
    System.out.println("reading from " + filePath);
    
    byte[] data;
    
    try
    {
     data = Files.readAllBytes(filePath);
    }
    catch (Exception e)
    {
     System.err.println("Exception when open file " + filePath);
     return;
    }
    
    strResult = port.putFilePart(fileId, partNumber, data, outString);
 
    if (!outString.value.isEmpty())
             {
     System.err.println("PutFilePart error: " + outString.value);
     return;
             }
    
    filePath = Paths.get(fileName + "." + String.format("%03d", ++partNumber));
   }
  }
  
  Holder<String> operationId = new Holder<String>();
  strResult = port.putData(fileId, operationId, outString);
  if (!outString.value.isEmpty())
          {
   System.err.println("PutData error: " + outString.value);
   return;
          }
  
  strResult = port.putDataActionResult(operationId.value, outString);
  if (!outString.value.isEmpty())
          {
   System.err.println("putDataActionResult error: " + outString.value);
   return;
          }
  else
  {
   System.out.println("putDataActionResult: " + strResult);
   
   while ("Active".equals(strResult))
   {
    //ждем в цикле 5 секунд - ожидаем окончания обработки данных на стороне конфигурации и спрашиваем снова
    try
    {
     Thread.sleep(1000);
    }
    catch (InterruptedException e)
    {
     //ignore
    }
    
    strResult = port.putDataActionResult(operationId.value, outString);
    
    if (!outString.value.isEmpty())
               {
     System.err.println("putDataActionResult error: " + outString.value);
     return;
               }
    System.out.println("putDataActionResult: " + strResult);
   }
  }
 }
 else
 {
  System.err.println("Connection failed, reason: " + outString.value);
 }
}

Ресурсы