Русский
Русский
English
Статистика
Реклама

Controller

Maple BUS в ореховой скорлупе или Периферия SEGA Dreamcast, как сделать

01.02.2021 10:05:55 | Автор: admin

И сразу к делу!

Протокол Maple BUS симметричный, то есть имея одну хорошую реализацию например HOST'а эту же реализацию можно использовать и как DEVICE. Проще, - можно читать джойстик, а можно им прикинуться.

Описание протокола (аппаратная часть).

Интерфейс Maple BUS двух-проводный. SDCKA/SDCKB, каждая из линий на определенных этапах выполняет роль как "передающая данные" и так и "защелкивающая данные".

Общение по шине Maple BUS осуществляется пакетами. Каждый пакет данных состоит из заголовочного паттерна, данных, контрольной суммы и завершающего паттерна. Максимальная длина пакета данных 1024 байта.

Всего паттернов 5 видов:

START - указывает на начало передачи данных (4-ре клока SDCKB в то время пока SDCKA в низком уровне).

Пакет всегда должен заканчиваться паттерном END (2-ва клока SDCKA пока SDCKB в низком уровне):

Occupancy паттерн - указывает на старт режима прослушивания шины (8-мь клоков SDCKB пока SDCKA в низком уровне). Переход линии HI->LO SDCKA после получения этого паттерна указывает на начало режима, LO->HI указывает на завершение. Этот режим используется для взаимодействия со световым пистолетом (Light GUN - Func. FT7):

RESET - аппаратный перезапуск устройства (14-ть клоков SDCKB пока SDCKA в низком уровне, только для DEVICE).

Теперь рассмотрим как по шине передаются данные.

Биты данных передаются фазами. В четной фазе линия данных - SDCKB, а клок - SDCKA, в нечетной наоборот (этот фрагмент тоже назовем паттерном :) ).

Величина таймаута на ответ от устройства после запроса хоста 1мс:

Помним, что например к джойстику можно подключать VMU, вибропак, микрофон...

Устройства подключаемые непосредственно к Maple BUS называются Device, а устройства подключаемые к Device называются Expansion Device, общение между Device и Expansion Device осуществляется средствами протокола LM-Bus. Expansion устройств можно подключить до 5-ти, хотя я не видел ни одного устройства в котором это было реализовано, а в чипах (например 315-6211-AB) "выведено наружу" только под 2-ва (хотя в программной части протокола под идентификацию EXP-DEV выделено пять бит, но тут честно говоря нужно уточнить, VMU например содержит память и LCD дисплей, это уже два Exp. устройства).

LM-BUS это что то типа суррогата Maple BUS, то есть шина на которую DEVICE напрямую переключает шину Maple BUS согласно тому какой Exp. DEVICE выбран HOST'ом.

LM-BUS тема отдельного разговора, отвлекаться не буду, перейдем к программной реализации протокола.

Программная часть протокола.

Как я уже писал выше данные передаются пакетами, рассмотрим пакет поближе:

  • COMMAND - команда, может принимать значения от 0x01 до 0xFE (см. возможные значения в коде ниже "maplebus.h").

maplebus commands
//HOST#defineDeviceRequest0x01#defineAllStatusRequest0x02#defineDeviceReset0x03#defineDeviceKill0x04#defineGetCondition0x09#defineGetMediaInfo0x0A#defineBlockRead0x0B#defineBlockWrite0x0C#defineGetLastError0x0D#defineSetCondition0x0E#defineFT4Control0x0F#defineARControl0x10#defineTransmitAgain0xFC//Device#defineDeviceStatus0x05#defineDeviceAllStatus0x06#defineDeviceReply0x07#defineDataTransfer0x08#defineARError0xF9#defineLCDError0xFA#defineFileError0xFB#defineTransmitAgain0xFC#defineCommandUnknown0xFD#defineFunctionTypeUnknown0xFE
  • DEST. AP - адрес назначения пакета (для какого устройства пакет).

  • ORIG. AP - от кого пакет.

Для AP справедлива следующая таблица:

PO[1:0] - Номер порта (A - 00, B - 01, C - 10, D - 11).

D/E - (1 - Device, 0 - Expansion Device или PORT).

LM[4:0] - (1 - Exp. DEVICE подключено, 0 - Слот Exp. пуст).

  • DATA SIZE - размер данных в пакете в 32-х битных чанках.

  • DATA - Состав пакета.

  • CRC - побайтный XOR всех данных включая COMMAND, AP, DATA SIZE, DATA.

"Общение" между HOST и DEVICE начинается с запроса DeviceRequest, в нем хост указывает какой порт он опрашивает, устройство, первый раз после включения или сброса "увидев" номер порта присваивает его себе (A/B/C/D).

Отвечать на данный запрос любое устройство обязано статусом (DeviceStatus answer):

Device ID - содержит функциональные возможности периферии (Device ID содержит блок FT, состав включенных битов в этом блоке определяет функции которые поддерживает устройство и FD - параметры поддерживаемых функций).

Device Functions
/*Device functions*/#define CONTROLLER    MAKE_DWORD(0x00000001)      //FT0 : Controller Function#define STORAGE            MAKE_DWORD(0x00000002)      //FT1 : Storage Function#define LCD                    MAKE_DWORD(0x00000004)      //FT2 : B/W LCD Function#define TIMER                MAKE_DWORD(0x00000008)      //FT3 : Timer Function#define AUDIO_INPUT    MAKE_DWORD(0x00000010)      //FT4 : Audio input device Function#define AR_GUN            MAKE_DWORD(0x00000020)      //FT5 : AR-Gun Function#define KEYBOARD        MAKE_DWORD(0x00000040)      //FT6 : Keyboard#define GUN                    MAKE_DWORD((unsigned int)0x00000080)        //FT7 : Light-Gun Function#define VIBRATION        MAKE_DWORD((unsigned int)0x00000100)        //FT8 : Vibration Function#define MOUSE                MAKE_DWORD((unsigned int)0x00000200)        //FT9 : Pointing Function#define EXMEDIA            MAKE_DWORD((unsigned int)0x00000400)        //FT10 : Exchange Media Function#define CAMERA            MAKE_DWORD((unsigned int)0x00000800)        //FT11 : Camera Device Functio

Destination code - указывает на целевой регион использования устройства.

Product name - название устройства (например {'D','r','e','a','m','c','a','s','t',' ','C','o','n','t','r','o','l','l','e','r', ' ',' ',' ',' ',' ',' ',' ',' ',' ',' '} - 30 байт).

License - кому принадлежит лицензия ( {'P','r','o','d','u','c','e','d',' ','B','y',' ','o','r',' ','U','n','d','e','r',' ','L','i','c','e','n','s','e',' ','F','r','o','m',' ','S','E','G','A',' ','E','N','T','E','R','P','R','I','S','E','S',',','L','T','D','.',' ',' ',' ',' ',' ',} -60 байт ).

Min./Max. current - соотв. минимальное и максимальное потребление устройства (1мА = 10 единиц, 43мА => 0x1AE).

Далее в пакете может идти "свободный статус устройства" (на изображении не указано, так как этот кусочек не обязателен), для джойстика он выглядит так: 40 байт "Version 1.000,1998/05/11,315-6125-AB Analog Module: The 4th Edition. 05/08".

То, какие команды применимы к устройству нам показывает блок Device ID.

К FT0, CONTROLLER, применима команда GetCondition - получить состояние кнопок/триггеров и аналоговых стиков геймпада. То в каких битах расположены какие значения указано всё в том же блоке Device ID. В частном случае, для геймпада Device ID будет выглядеть так:

В ответ на запрос GetCondition, джойстик обязан отправить отчет о состоянии кнопок, выглядит он следующим образом:

Ra/La/Da/Ua - Право/Лево/Вниз/Вверх (цифровой "крестик").

Start/A/B/X/Y - соотв кнопки.

A1, A2 - аналоговые курки

A3 и A4 - положение "стика".

Вот собственно и всё что нужно знать для реализации контроллера.

Реализация (аппаратная часть)

В общем то можно взять линии SDCKA и SDCKB и прикрутить на прерывания микроконтроллера и чисто программно реализовать, однако если МК медленный, то успевать не будет, и это не самое главное, во первых "плотность данных" для разных устройств разная и если например джойстик работать будет на одной программной реализации, то не факт что будет работать вибропак или карта памяти, во вторых в программной реализации определение ошибок внутри фрейма как и скажем команда "аппаратный сброс" не реализуемы, поэтому правильнее будет сделать аппаратный кусочек приемника, а отправлять и микроконтроллером можно.

Возьмем CPLD попроще (EPM3032) и реализуем xMAPLE:

SDCKA/SDCKB - вход линий Maple BUS.

GCLK - внешний CLK 16-48MHz.

INHTxD - сигнал блокировки работы приемника, 1 - игнорировать события на шине, 0 - нормальное функционирование.

RxD - идет прием пакета.

nSTRCV- начат прием пакета (Rising Edge).

nDLatch - Негативный импульс для "защелки данных" (сигнализирует о том что на линии данных Q[7..0] присутствует следующий полученный байт).

Q[7..0] - шина данных.

EOP - получен паттерн END (конец приема пакета).

FERR - обнаружена ошибка при приеме пакета.

nRST - подключается напрямую к микроконтроллеру - если получен RESET паттерн, - 0.

И общий вид:

Пишем это на верилог'е (3 файлика, надеюсь догадаетесь как это соединять):

SMAPLE.v
module SMAPLE(input GCLK,//MCU Generated 16MHz clock inputinput INHTxD,//Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA)input SDCKAi,//Data/Clock A Lineinput SDCKBi,//Data/Clock B Lineoutput RxD,//Receive on progress (While receive is 1)output [7:0]Q// Output data bus (MCU can read valid data on this //port in time 200uS after data latch Negative Pulse received),output nSTRCV,//Receive start, negative pulse - Outputoutput OCPYi,//Occupancy packet received - Outputoutput nRST,//Reset packet received - Outputoutput FERR,//Frame error - Outputoutput EOPi,//End Of Packed received - Outputoutput nDLatch//New Data latched on BUS (Negative Pulse));/*Control Register*/reg rRxD = 0;assign RxD = rRxD;reg rFERR = 0;assign FERR = rFERR;wire nWE;assign nDLatch = (EOPi & nWE);wire iFERR;/* Align Data Packet */reg rENA = 1'b0;reg rENB = 1'b0;always @(posedge GCLK or negedge nRST) beginif(!nRST) beginrENA <= 1'b0;rENB <= 1'b0;end else beginrENA <= SDCKAi;rENB <= SDCKBi;endendalways @(posedge GCLK or negedge nRST) beginif(!nRST) beginrFERR <= 0;rRxD <= 0;end else beginif(!EOPi)// && !INHTxD) rRxD <=0 ;else beginif(!iFERR) rFERR <= 1;if(!nSTRCV) beginrFERR <= 0;rRxD <= ~INHTxD;endendendendline_monitor line_monitor(.GCLK(GCLK),//Global Clock - Input.SDCKA(SDCKAi|INHTxD),//CLOCK/DATA Line A disabled by data transmit - Input.SDCKB(SDCKBi|INHTxD),//CLOCK/DATA Line B disabled by data transmit - Input.RxDr(RxD),//Data Receive in progress - Input.RxD(nSTRCV),//Receive start, negative pulse - Output.OCPY(OCPYi),//Occupancy packet received - Output.RESET(nRST),//Reset packet received - Output.FERR(iFERR),//Frame error - Output.EOP(EOPi),//End Of Packed received - Output.ENA(rENA),//CLOCK For Line B.ENB(rENB)//CLOCK For Line A );/*Receive Maple Frame*/maple_receive maple_receive(.SDCKA(SDCKAi),//CLOCK/DATA Line A disabled by data transmit - Input.SDCKB(SDCKBi),//CLOCK/DATA Line B disabled by data transmit - Input.ENA(rENA),//CLOCK For Line B.ENB(rENB),//CLOCK For Line A .RCV(RxD),//Receive in progress, 1 - receive - Input.Dout(Q[7:0]), //Received data byte - Output.nWE(nWE),//Write Latch - Output.RxDi(nSTRCV),//Receive start, negative pulse - Input.INHTxD(INHTxD)//Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA));endmodule
line_monitor.v
module line_monitor(input GCLK,input SDCKA,input SDCKB,input  RxDr,//Data Receive in progress - Inputoutput RxD,output OCPY,output RESET,output FERR,output EOP,input ENA,input ENB);reg [3:0] countA = 0;reg [2:0] countB = 0;reg [3:0] pcount = 0;reg rEOP = 1'b1;assign EOP = rEOP;assign RxD = (pcount == 4'h4) ? 1'b0 : 1'b1;assign OCPY = (pcount == 4'h8) ? 1'b0 : 1'b1;assign RESET = (pcount == 4'hE)? 1'b0 : 1'b1; //Output reset signal does not need to check for FERRassign FERR = (!((RxD & OCPY & RESET) && pcount[3:1])) | (!RxDr & !rEOP);//assign EOP = (eopcount == 3'h2) ? 1'b0 : 1'b1;always @(posedge SDCKA) pcount <= countA;always @(posedge SDCKB) rEOP <= !(countB == 3'h2);//Patterns//PATTERN Counter Managingalways @(posedge ENA or negedge ENB) beginif (ENA) begincountA <= 0;endelse begincountA <= countA + 4'h1;endend //EOP Counter Managingalways @(posedge ENB or negedge ENA) beginif (ENB) begin countB <= 0;endelse begin countB <= countB + 3'h1;endend //synopsys translate_off//synopsys translate_onendmodule
maple_receive.v
module maple_receive(input SDCKA,//CLOCK/DATA Line Ainput SDCKB,//CLOCK/DATA Line Binput ENA,//CLOCKinput ENB,//CLOCKinput RCV,//Receive in progress, 1 - validoutput [7:0]Dout, //received data outputoutput nWE,input RxDi,input INHTxD//Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA));reg [3:0] dataA = 4'h0;reg [3:0] dataB = 4'h0;reg [1:0]countB = 2'b00;reg rLastBitCounted = 1'b1;//B LINE//Dout[1] = SDCKA Means Major version 1.//Dout[0] = SDCKB Means Minor version .0//And version result = 1.0assign Dout[1] = !INHTxD ? dataB[0] : SDCKA;assign Dout[3] = dataB[1];assign Dout[5] = dataB[2];assign Dout[7] = dataB[3];//A LINEassign Dout[0] = !INHTxD ? dataA[0] : SDCKB;assign Dout[2] = dataA[1];assign Dout[4] = dataA[2];assign Dout[6] = dataA[3];assign nWE = (dtaLock);always @(negedge ENA)begindataB[3:1] <= dataB[2:0];dataB[0] <= SDCKB;if(RCV) begincountB <= countB + 2'b1;end else begincountB <= 2'b11;endendalways @(negedge ENB)begindataA[3:1] <= dataA[2:0];dataA[0] <= SDCKA;rLastBitCounted <= !countB[0] | !countB[1];endwire dtaLock = rLastBitCounted;endmodule

Чтобы не "развлекаться с проводочками" накидал Eval Board.

Общий вид по блокам:

Полная схема модуля.

Gerber фалы.

Внешний вид:

И посадочное место под Eval...

Gerber файлы.

И соединяем всё это вместе:

Реализация устройства.

Железки есть, схемы есть, переходим к реализации.

Для начала заделаем небольшой код чтобы чтобы геймпад Dreamcast прикидывался геймпадом XBOX360 (поскольку я заботливо "выкусил хэндшейк" с XBOX360, данная реализация на приставке работать не будет только на ПК).

И, опять таки чтобы не на проводках, делаем плату коннектора для джойстика из двух половинок:

Верхняя часть (GERBER), нижняя часть (GERBER).

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

Для этой цели можно к примеру разобрать разъем SD вот как-то так:

Прикинем как должен работать алгоритм... не буду останавливаться на том как работает USB HID, опишу общую схему опроса устройств на шине MAPLE.

И собственно архив с исходниками.

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

  • USE_STDPERIPH_DRIVER - использовать стандартную библиотеку периферии от ST.

  • STM32F10X_MD - чип Medium Density.

  • MAPLE_HOST - библиотека MAPLE работает в режиме HOST.

  • USB_HID - собрать целевое HID устройство.

Прошиваем, подключаемся:

и видим вот такую картину (не забываем что необходимо поставить Microsoft Xbox 360 Accessories, а ещё помним что геймпад у нас работает в режиме XInput... кому лень разбираться, можно скачать уже откомпилированную прошивку):

А теперь развернем xMAPLE в обратную сторону и...

Подключим мышь от ПК к DREAMCAST.

Мышь, - FT9 : Pointing Function.

Что нам нужно, DeviceID и состав команды GetCondition, чтоб собирать пакет с данными.

Mouse DeviceID:

Стандартная мышь Dreamcast содержит 3 кнопки: A,B,W, дельты смещения по осям X/Y: AC1,AC2 (ball) и смещение "колеса": AC3 (wheel).

AC1,AC2,AC3 - десяти-битные величины плюс флаг переполнения.

Вот так выглядит пакет данных:

AOV2, AOV1, AOV0 - флаги переполнения для AC3, AC2, AC1 соответственно.

Для удобства накидаем схемку адаптера PS/2 для нашей борды:

...разводим, получаем gerber'ы...

И с завода нам приезжает вот это:

Ну и чтобы совсем удобно, накидаем вот такую схему, если брать провод от оригинального пада, то просто подключаемся к разъему и УРА.

"Рисуем" gerber'ы и получаем вот такой переходник:

Собираем весь этот "огород" вместе:

Компилируем прошивку (ниже архив с исходниками) не забывая объявить константы препроцессора:

  • USE_STDPERIPH_DRIVER - использовать стандартную библиотеку периферии от ST.

  • STM32F10X_MD - чип Medium Density.

  • MAPLE_DEVICE - библиотека MAPLE работает в режиме DEVICE.

  • EN_MOUSE - собрать целевое HID устройство.

  • MOUSE_CALLBACK - обработать функцию чтения мыши в процессе ожидания запроса от HOST.

  • EXTI9_5_CALLBACK - передавать в код пользователя системные прерывания EXTI5-EXTI9 библиотеки MAPLE_BUS.

(исходники, скомпилированный HEX).

К слову, если вместо EN_MOUSE в данных исходниках определить константу EN_CONTROLLER, то мы получим довольно забавную штуку, переходник превращающий PS/2 мышь в контроллер DREAMCAST, собственно специально сделал, потому как мышью в меню DREAMCAST управлять нельзя. Поэтому чтобы наглядно увидеть работоспособность исходников и оборудования в целом не запуская скажем "HALF LIFE для проверки" можно прошить откомпилированный код с константой EN_CONTROLLER и управлять внутри меню мышкой PS/2.

Прошиваем, подключаемся к DREAMCAST и оно работает!!!

Вот собственно и всё что хотел поведать. Однако я не рассказал о (надеюсь ещё расскажу :) ):

  • Как работать с VibroPAK.

  • Как реализовать Memory Unit (хотя на борде расширения PS/2 SPI EEPROM память можно установить и работать с ней).

  • И у меня остались комплекты печатных плат и трём желающим "попробовать свои силы" могу отправить комплекты печатных плат за стоимость почты.

Удачного дня! Отличного настроения и взаимопонимания!!!

Подробнее..

Обзор возможностей Kubespray Отличие оригинальной версии и нашего форка

20.09.2020 14:22:27 | Автор: admin

23 сентября 20.00 МСК Сергей Бондарев проведёт бесплатный вебинар Обзор возможностей Kubespray, где расскажет, как готовят kubespray, чтобы получилось быстро, эффективно и отказоустойчиво.


Сергей Бондарев расскажет отличие оригинальной версии и нашего форка:



Отличие оригинальной версии и нашего форка.


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


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


  • собирал кластер etcd;
  • устанавливал кублеты, генерил сертификаты, конфиги и токены доступа для статик подов контролплейна и прочих служебных компонентов;
  • создавал сервис аккаунты для рабочих узлов и подключал их в кластер.

Но в позапрошлом году они этот функционал выпилили, оставив только кубадм. Который в то время был не очень. Мне стало обидно и я сделал свой форк, в котором сохранил классический режим установки, и собственно теперь поддерживаю этот форк в актуальном состоянии, черри-пикая коммиты из оригинального кубспрея к себе. Попутно допиливая классический режим под новые изменения.


В итоге разница между кластерами, созданными моим форком и оригинальным это kube-proxy и сроки действия сертификатов.


В моем форке все осталось, как было раньше куб-прокси запускается, как статик под, сертификаты выписываются на 100 лет.


В Kubeadm куб-прокси запускается, как daemonset, а сертификаты выписываются на 1 год, и их надо периодически продлевать. kubeadm наконец-то научился это делать одной командой.


Разница небольшая, и на сегодня мы используем оба варианта.


Особенности (недостатки) при промышленной эксплуатации:


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


Сценарий сложный, есть нелогичные места, тяжелое наследие legacy. Установка доп. контроллеров и софта через кубспрей хороша для обучения и тестов. В пром. эксплуатации зависеть от кубспрея не очень здравая идея, плюс обновление софта реализовано методом "убил-сделал новый" значит перерыв в обслуживании.


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


Например у меня была проблема с кубадмом, когда он падал в момент добавления второго и третьего мастера, и после этого кубспрей делал kubeadm reset на узле, и пробовал добавить мастер еще раз.


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


Opensource как он есть.


Всё это и многое другое на бесплатном вебинаре Обзор возможностей Kubespray 23 сентября 20.00 МСК.


Присоединяйтесь!

Подробнее..

Partial Update library. Частичное обновление сущности в Java Web Services

17.02.2021 12:10:47 | Автор: admin

Введение

В структуре веб-сервисов типичным базовым набором операций над экземплярами сущностей(объектами) является CRUD (Create, Read, Update и Delete). Этим операциям в REST соответствуют HTTP методы POST, GET, PUT и DELETE. Но зачастую у разработчика возникает необходимость частичного изменения объекта, соответствующего HTTP методу PATCH. Смысл его состоит в том, чтобы на стороне сервера изменить только те поля объекта, которые были переданы в запросе. Причины для этого могут быть различные:

  • большое количество полей в сущности;

  • большая вероятность одновременного изменения одного и того же объекта при высокой нагрузке, в результате которого произойдет перезапись не только модифицируемых полей;

  • невозможность или более высокая сложность изменения полей в нескольких или всех объектах в хранилище(bulk update);

Эти и, возможно, другие причины побуждают разработчика реализовать стек операций по частичному изменению объекта.

Рассмотрим наиболее часто применяемые варианты решения задачи частичного обновления.

Использование обычного контроллера и DTO

Один из наиболее часто встречаемых вариантов реализации метода PATCH. В контроллере пришедший объект десериализуется в обычный DTO, и далее по стеку слоев приложения считается, что все поля в DTO со значением null не подлежат обработке.

К плюсам данного метода можно отнести "привычность" реализации.

К минусам относится во-первых потеря валидности значения null для обработки (после десериализации мы не знаем отсутствовало ли это поле в передаваемом объекте или оно пришло нам со значением null).

Вторым минусом является необходимость явной обработки каждого поля при конвертировании DTO в модель и далее по стеку в сущность. Особенно сильно это чувствуется в случае обработки сущностей с большим количеством полей, сложной структурой. Частично вторую проблему возможно решить с использованием ObjectMapper(сериализация/десериализация POJO, аннотированных @JsonInclude(Include.NON_NULL) ) ,а так же библиотекой MapStruct, генерирующей код конвертеров.

Использование Map<String, Object> вместо POJO

Map<String, Object> является универсальной структурой для представления данных и десериализации. Практически любой JSON объект может быть десериализован в эту структуру. Но, как мы можем понять из типов обобщения, мы теряем контроль типов на этапе компиляции (а соответственно и на этапе написания исходного кода в IDE).

К достоинствам этого метода можно отнести универсальность представления данных и возможность корректной обработки значения null.

Недостатки же в данном случае глобальны: потеря контроля типов на этапе написания исходного кода и компиляции, необходимость явного приведения типов, высокая сложность рефакторинга, обнаружение практически всех ошибок, связанных с типами данных и именованием полей только в runtime(тесты или реальный запуск приложения).

Использование JSON Patch и JSON Merge Patch

JSON Patch и JSON Merge Patch являются стандартизованными и наиболее универсальными методами описания частичного изменения объекта. Спецификация Java EE содержит интерфейсы, описывающие работу с обоими этими форматами: JsonPatch и JsonMergePatch. Существуют реализации этих интерфейсов, одной из которых является библиотека json-patch. Оба формата кратко описаны в статье Michael Scharhag REST: Partial updates with PATCH.

Достоинства метода: стандартизация, универсальность, возможность реализовать не только изменение значения в объекте, но и добавление, удаление, перемещение, копирование и проверку значений полей в объекте.

К недостаткам метода можно отнести несколько большую сложность структуры данных, описывающих изменения в объекте и ее визуальное несоответствие типовому DTO объекта, более высокую сложность передачи данных об изменениях между слоями приложения, усложнение процесса валидации, усложненный процесс преобразования структуры описания изменений к запросам множественного обновления объектов в БД, etc.

Partial Update library

Основной целью создания библиотеки стало объединение положительных и исключение отрицательных сторон первых двух методов из описанных: использование классических DTO в фасадах и гибкости структуры Map<String, Object> "под капотом".

Ключевыми элементами библиотеки являются интерфейс ChangeLogger и класс ChangeLoggerProducer.

Класс ChangeLoggerProducer предназначен для создания "оберток" POJO, перехватывающих вызовы сеттеров и реализующих интерфейс ChangeLogger для получения изменений, произведенных вызовами сеттеров в виде структуры Map<String, Object>.

Для дальнейших примеров будут использоваться вот такие POJO:

public class UserModel {private String login;private String firstName;private String lastName;private String birthDate;private String email;private String phoneNumber;}@ChangeLoggerpublic class UserDto extends UserModel {}

Вот пример работы с такой "оберткой":

ChangeLoggerProducer<UserDto> producer = new ChangeLoggerProducer<>(UserDto.class);UserDto user = producer.produceEntity();user.setLogin("userlogin");user.setPhoneNumber("+123(45)678-90-12");Map<String, Object> changeLog = ((ChangeLogger) user).changelog();/*    changeLog in JSON notation will contains:    {        "login": "userlogin",        "phoneNumber": "+123(45)678-90-12"    }*/

Суть "обертки" состоит в следующем: при вызове сеттера его имя добавляется в Set<String>, при дальнейшем вызове метода Map<String, Object> changelog() он вернет ассоциативный список, ключом в котором будет имя поля, а соответствующим ключу значением будет объект, возвращенный соответствующим геттером. В случае, если объект, возвращаемый геттером реализует интерфейс ChangeLogger, то в значение поля пойдет результат вызова метода Map<String, Object> changelog().

Для сериализации/десериализации "оберток" реализован класс ChangeLoggerAnnotationIntrospector. Это класс представляет собой Annotation Introspector для ObjectMapper. Основной задачей класса является создание "обертки" при десериализации класса, аннотированного @ChangeLogger аннотацией библиотеки и сериализацией результата вызова метода Map<String, Object> changelog() вместо обычной сериализации всего объекта. Примеры использования ObjectMapper с ChangeLoggerAnnotationIntrospector приведены ниже.

Сериализация:

ObjectMapper mapper = new ObjectMapper.setAnnotationIntrospector(new ChangeLoggerAnnotationIntrospector());ChangeLoggerProducer<UserDto> producer = new ChangeLoggerProducer<>(UserDto.class);UserDto user = producer.produceEntity();user.setLogin("userlogin");user.setPhoneNumber("+123(45)678-90-12");String result = mapper.writeValueAsString(user);/*    result should be equal    "{\"login\": \"userlogin\",\"phoneNumber\": \"+123(45)678-90-12\"}"*/

Десериализация:

ObjectMapper mapper = new ObjectMapper.setAnnotationIntrospector(new ChangeLoggerAnnotationIntrospector());String source = "{\"login\": \"userlogin\",\"phoneNumber\": \"+123(45)678-90-12\"}";UserDto user = mapper.readValue(source, UserDto.class);Map<String, Object> changeLog = ((ChangeLogger) user).changelog();/*    changeLog in JSON notation will contains:    {        "login": "userlogin",        "phoneNumber": "+123(45)678-90-12"    }*/

Используя ObjectMapper с ChangeLoggerAnnotationIntrospector мы можем десериализовать пришедший нам в контроллер JSON с полями для частичного апдейта и далее передавать эти данные в подлежащие слои сервисов для реализации логики. В библиотеке присутствует инфраструктура для реализации мапперов DTO, Model, Entity с использованием "оберток". Пример полного стека приложения реализован в тестовом проекте Partial Update Example.

Итог

Partial Update library позволяет с рядом ограничений реализовать наиболее типичную задачу частичного изменения объекта, используя максимально близкий к типовому способ организации стека приложения. При этом универсальность ассоциативного списка объединена с сохранением контроля типов данных в процессе написания исходного кода и компиляции, что позволяет избежать большого числа ошибок в runtime.

В настоящее время функционал имеет ряд ограничений:

  • реализован только простейший маппинг "поле в поле", что не позволяет автоматизировать ситуации с разными именами одного и того же поля в DTO, Model, Entity;

  • не реализован модуль интеграции со Spring, в связи с чем для реализации сериализации/десериализации "оберток" DTO необходимо реализовать конфигурацию(как в примере), добавляющую ChangeLoggerAnnotationIntrospector в стандартный ObjectMapper контроллера приложения;

  • не реализованы утилиты формирования SQL/HQL запросов для bulk update операций с БД;

В последующих версиях планируется добавление недостающего функционала.

Формат данной статьи не позволяет более детально рассмотреть инфраструктуру для создания мапперов и показать применение библиотеки в типичном стеке приложения. В дальнейшем я могу более детально разобрать Partial Update Example и уделить больше внимания описанию внутренней реализации библиотеки.

Подробнее..
Категории: Java , Api , Rest , Patch , Controller

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru