Приветствую, Дорогие Друзья.
Продолжаем цикл статей, освещающий деятельность (бурную) нашей некоммерческой организации.
Как и обещал — переходим от простого (логирование) к более сложному: метапрограммирование.
Завязка
Так сложилось, что нашей материнской корпорации (крупный фин-тех) потребовалось интегрироваться с другой крупной организацией, использующей мейнфреймы и 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
