Путь из COBOL в Java: пишем транспилятор за сутки

от автора

Приветствую, Дорогие Друзья.

Продолжаем цикл статей, освещающий деятельность (бурную) нашей некоммерческой организации.

Как и обещал — переходим от простого (логирование) к более сложному: метапрограммирование.

Завязка

Так сложилось, что нашей материнской корпорации (крупный фин-тех) потребовалось интегрироваться с другой крупной организацией, использующей мейнфреймы и COBOL.

Казалось бы — в чём сложность, и на что это может повлиять?

Дело в том, что будучи ведомой стороной в этой интеграции, нам необходимо было поддержать интерфейсы обмена данными с внешней стороны. А именно — бинарные файлы данных "COBOL data files".

Как выяснилось (а мы до этого не имели опыта работы с COBOL), файлы данных в нём имеют нелинейную структуру. А именно:

  • Один и тот же блок данных может быть интерпретирован как разные "группы" (формат данных в терминологии COBOL), эдакий полиморфизм данных: директива redefines. При том — со всеми вложенными группами и их полями.
  • Блок может повторяться N раз: директива occurs
  • Блоки идут последовательно, но их начало отсчитывается от конца предыдущего блока — соответственно, нельзя прочитать отдельный блок, не прочитав предыдущие блоки.

Осложнялось всё это тем, что спецификации файлов представляли собой исполняемый исходный код COBOL: "copybook" (в части Data Section Division), вместе со всеми возможными вариантами синтаксиса описания формата данных, например:

PIC X(06)V99

или

 PIC 9(06).9999999

или

PIC 9(07)V9(07) COMP-3

Добавим к этому:

  • Хитрый бинарный формат хранения чисел IBM Computational 3
  • Различные кодировки (EBCDIC, ASCII) — даже для одного типа файлов (на Тесте и на Проде)
  • Различные переносы строки (CR, LF, CRLF, LFCR, Newline, неявные после N байт)
  • Огромные копибуки по 400 записей с кучей полей
  • Копибуки имеющие структуру с header, record, trailer и их комбинациями

И вот тут это всё вместе становится уже реальной проблемой: как это поддержать?

Мы начали с исследования, какие есть существующие решения. И результат был просто катастрофичен: из бесплатных решений был только JRecord из 90х.

Но открыв его исходный код, стало ясно — он мёртв и не пригоден к использованию, из-за качества и устаревания. Да он банально отказался читать наши тестовые файлы, а реализация data picture и comp-3 была сделана совершенно ненаучно.

Сроки поджимали, и оставалось 2 варианта:

  • Захардкодить всё
  • hold my coffee

Но природная лень сыграла своё, и конечно никто ничего не стал хардкодить. Чтобы сделать правильно — а заодно сэкономить силы и время — просто взяли и реализовали единственно правильное решение вышеописанной проблемы в комплексе:

  • реализовали транспилятор COBOL в Java (Groovy)

Звучит страшно, не так ли? Но на самом деле всё оказалось быстро — и сейчас я расскажу как у нас получилось сделать это всего за 1 сутки.

Развязка

Чтобы реализовать какой-то язык программирования, требуется пара вещей:

  • Парсер исходного кода
  • Компилятор (или транспилятор в существующий язык)

Нам не требовалось поддерживать весь синтаксис COBOL — только часть относящуюся к Data Section Division. Поэтому, наиболее трудозатратным становилось бы написание парсера исходного кода COBOL.

К счастью, быстрый поиск привёл к одному современному старт-апу, специализирующемуюся на переносу экосистем на COBOL в облако (на платформе Java) — и они опубликовали исходный код своего парсера под лицензией MIT:
https://github.com/uwol/proleap-cobol-parser

Дело оставалось за малым, реализовать:

  • Транспилятор в Java (Groovy) код
  • Среду исполнения этого кода
  • API, позволяющий осуществить интеграцию с внешней экосистемой (ETL)

Используемый парсер исходного сделан на основе Antlr, и предоставляет meta-API в виде шаблона Visitor. Это достаточно распространённый подход в мета-программировании.

Для реализации транспилятора нужно было просто реализовать visit методы для поддерживаeмого синтаксиса, формируя Groovy код, например для блока указания формата данных (data picture clause):

    @Override     @CompileDynamic     Boolean visitDataPictureClause(CobolParser.DataPictureClauseContext ctx) {         PictureClause entry = (PictureClause) program.getASGElementRegistry().getASGElement(ctx)         def (length, comp3length, scale) = calculateLengths(entry.pictureString)         write """    setDataPicture("""         write """        depth: ${currentFrame.depth},"""         write """        pictureString: "${entry.pictureString}","""         write """        length: ${length},"""         write """        comp3length: ${comp3length},"""         write """        scale: ${scale},"""         Boolean result = super.visitDataPictureClause(ctx)         write """    )"""         return result     }

Как видно, код COBOL преобразуется в код Groovy.
Весь вывод собирается в исходный код класса, представляющий собой весь copybook.
Далее этот класс компилируется в Java класс, представляющий собой реализацию базовой среды исполнения.

Здесь очень кстати пришлась поддержка Closure в Groovy — именно Closure представляет собой тот мостик между процедурами и иерархическим кодом COBOL.

Вот пример, как выглядит исходный код на COBOL (copybook):

000010 IDENTIFICATION  DIVISION.                                        XXXXXXXX        PROGRAM-ID.     UnstringSample.                                  XXXXXXXX        ENVIRONMENT     DIVISION.                                        XXXXXXXX        CONFIGURATION SECTION.                                           XXXXXXXX        SPECIAL-NAMES.   DECIMAL-POINT IS COMMA.                         XXXXXXXX        INPUT-OUTPUT    SECTION.                                         XXXXXXXX        DATA            DIVISION.                                        XXXXXXXX        WORKING-STORAGE SECTION.                                         XXXXXXXX        01 ABCDE-RECORD.                                                 XXXXXXXX XXXXXX  02 ABCDE-REC.                                                   XXXXXXXX         03 ABCDE-COMMON.                                                XXXXXXXX            05 ABCDE-DETAILS.                                            XXXXXXXX              10 ABCDE-RECORD-ABC.                                       XXXXXXXX                 15 ABCDE-PRI-ABC.                                       XXXXXXXX                    20 ABCDE-ABC-AAAAAAAA         PIC X(02).             XXXXXXXX                    20 ABCDE-ABC-ACCT-ABCS.                              XXXXXXXX                       25 ABCDE-ABC-ABC-1         PIC X(02).             XXXXXXXX                       25 ABCDE-ABC-ABC-2         PIC X(03).             XXXXXXXX                       25 ABCDE-ABC-ABC-3         PIC X(03).             XXXXXXXX                       25 ABCDE-ABC-ABC-4         PIC X(04).             XXXXXXXX

И вот во что он преобразуется:

io.infinite.cobol.CobolCompiler|import groovy.transform.CompileStatic import io.infinite.cobol.CobolRuntime import io.infinite.cobol.CobolApi import io.infinite.other.CopybookStructureEnum  @CompileStatic class CobolClosureRuntime extends CobolRuntime {      @Override     void run(Long totalSize, InputStream inputStream, String charsetName, List<Byte> lineBreakBytes, CobolApi cobolApi, CopybookStructureEnum copybookStructure) {         super.setup(totalSize, inputStream, charsetName, lineBreakBytes, cobolApi, copybookStructure) readFile() {     createRecord("ABCDE-RECORD") {         createGroup(2, "ABCDE-REC") {             createGroup(3, "ABCDE-COMMON") {                 createGroup(4, "ABCDE-DETAILS") {                     createGroup(5, "ABCDE-RECORD-ABC") {                         createGroup(6, "ABCDE-PRI-ABC") {                             createGroup(7, "ABCDE-ABC-AAAAAAAA") {                                 setDataPicture(                                     depth: 7,                                     pictureString: "X(02)",                                     length: 2,                                     comp3length: 2,                                     scale: 0,                                 )                             }//<(end of group: ABCDE-ABC-AAAAAAAA)                             createGroup(7, "ABCDE-ABC-ACCT-ABCS") {                                 createGroup(8, "ABCDE-ABC-ABC-1") {                                     setDataPicture(                                         depth: 8,                                         pictureString: "X(02)",                                         length: 2,                                         comp3length: 2,                                         scale: 0,                                     )                                 }//<(end of group: ABCDE-ABC-ABC-1)                                 createGroup(8, "ABCDE-ABC-ABC-2") {                                     setDataPicture(                                         depth: 8,                                         pictureString: "X(03)",                                         length: 3,                                         comp3length: 2,                                         scale: 0,                                     )                                 }//<(end of group: ABCDE-ABC-ABC-2)                                 createGroup(8, "ABCDE-ABC-ABC-3") {                                     setDataPicture(                                         depth: 8,                                         pictureString: "X(03)",                                         length: 3,                                         comp3length: 2,                                         scale: 0,                                     )                                 }//<(end of group: ABCDE-ABC-ABC-3)                                 createGroup(8, "ABCDE-ABC-ABC-4") {                                     setDataPicture(                                         depth: 8,                                         pictureString: "X(04)",                                         length: 4,                                         comp3length: 3,                                         scale: 0,                                     )                                 }//<<<<(end of group: ABCDE-ABC-ABC-4)                             }//<<<<(end of group: ABCDE-ABC-ACCT-ABCS)                         }//<<<<(end of group: ABCDE-PRI-ABC)                     }//<<<<(end of group: ABCDE-RECORD-ABC)                 }//<<<<(end of group: ABCDE-DETAILS)             }//<<<<(end of group: ABCDE-COMMON)         }//<<<<(end of group: ABCDE-REC)     }//<<<<(end of group: ABCDE-RECORD) }//<<<<<      }  }

Финал

В момент запуска проекта выяснилось, что Production файлы отличаются по формату от тестовых.

Если бы захардкодили всё — пришлось бы тратить пол дня, чтобы понять что изменилось и сделать исправления. А так — подложили новые copybook — и всё заработало.

За всё время эксплуатации не было выявлено ни одного дефекта.
Еженедельно обрабатываются файлы на миллиарды $.

В результате получился уникальный продукт:

  • Единственная Open Source реализация COBOL на Java (Groovy)
  • включает в себя наилучший доступный парсер исходного кода COBOL (proleap.io)
  • Поддерживает бинарные структуры данных COMP-3
  • На данный момент поддерживает только Data Section Division
  • Поддерживает директиву redefines
  • Поддерживает директиву occurs
  • Отлично работает с иерархическими API, например XML (часть поставки)
  • Поддерживает файлы с заголовком и трейлером
  • Поддерживает различные кодировки (EBCDIC, ASCII и другие)
  • Поддерживает настраиваемые символы переноса строки

Заключение

1) Не бойтесь идти против системы и мыслить нестандартно
2) Готовьтесь заранее — изучайте технологии, практикуйтесь. В нужный момент каждое знание потребуется.
3) Не верьте тем, кто говорит "всё сделано до нас". В 2020 году есть огромный простор для работы.

Некоммерческая организация https://i-t.io в свою очередь сделает всё возможное, чтобы заполнить этот простор. В следующих статьях вы узнаете как мы собираемся совершить революцию в области безопасности веб-сервисов, а также мы представим нашу инновационную операционную модель.

Оставайтесь с нами.

Всех благ!

Исходный код проекта:
https://github.com/INFINITE-TECHNOLOGY/COBOL

ссылка на оригинал статьи https://habr.com/ru/post/489730/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *