Java

Реализация системы конвертирования видео

 

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

В данной статье мы расскажем о нашей точке зрения на внедрение подобных систем, приведем примеры функций системы, используемые для существующих приложений. Демо-версия нашей системы написана на языке Java (Spring + Hibernate), в ней использованы FFmpeg в качестве конвертера, JMS для взаимодействия с внешней системой и для внутренних коммуникаций.

Цели системы

Система конвертации видео предназначена для выполнения следующих задач:

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

В дополнение ко всему вышеперечисленному, система видео конвертации должна соответствовать следующим техническим требованиям:

  • Система должна быть полностью независима как от основного приложения, так и от доменной модели или архитектуры внешнего приложения, все это дает возможность использовать систему неоднократно
  • Обеспечивать высокий уровень масштабируемости
  • Система должна быть надежной
  • Система также должна предотвращать потерю данных

Архитектура

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

Компоненты системы

Система состоит из диспетчера по конвертации видео и нескольких клиентов для конвертации видео, непосредственно проводящих конвертацию.

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

Также диспетчер предусматривает необходимые опции для управления и мониторинга системы.

Клиент конвертирует видео, используя FFmpeg, запуская его как отдельный процесс операционной системы и затем в случае,если конвертация прошла успешно, сохраняет ее результаты в хранилище ресурсов.

Каждый клиент обладает уникальным именем, назначаемым системным администратором. Схема 1:

Краткое описание работы

Рабочие процессы системы могут быть представлены в виде следующей схемы:

  • Внешняя система генерирует событие, в котором бы содержатся все необходимые для конвертации данные. Событие обрабатывается сервером конвертации видео
  • Во время обработки события, сервер создает задачу конвертации видео, сохраняет ее в базу данных, затем, если присутствуют свободные клиенты, задача перенаправляется к одному из клиентов для ее выполнения. Если свободных клиентов в данный момент нет, то задача ставится в режим ожидания. Сервер вернется к этой задаче тогда, когда появится свободный клиент. Внутренний JMS используется для передачи задач.
  • Клиент получает задачу, определяет, где находится искомый ресурс. Он выбирает подходящее соединение с хранилищем и загружает ресурс.
  • Клиент посылает сообщение, сообщающее серверу о том, что началась конвертация видео, запускает FFmpeg как отдельный процесс и ожидает его окончания. Если FFmpeg не выключается после некоторого времени, то клиент сам завершает его и сообщает об ошибке серверу, путем посылки JMS сообщения. Сообщение об ошибке также формируется, если FFmpeg завершил свою работу с ошибкой.
  • Если FFmpeg завершает свою работу корректно и итоговый файл содержит необходимую информацию, считается, что конвертация прошла успешно. Клиент загружает результат в хранилище ресурсов и уведомляет сервер, посылая ему JMS сообщение.
  • Сервер перенаправляет информацию о начале и завершении конвертации видео в основную систему.

Процессы коммуникации

Как уже было сказано выше, вся коммуникация осуществляются через JMS. Диспетчер сообщается с внешней системой, используя JMS topic. Для внутренней коммуникации мы также используем JMS topic.

Для того, чтобы облегчить процесс введения системы в действие, мы используем объектные сообщения и вся информация посылается как объект (событие-объект) внутри сообщения.

Подробнее о процессах коммуникации:

  • При запуске сервер посылает сообщения всем клиентам для того, чтобы определить их номера и статусы. Также сервер просматривает имеющиеся задачи, чтобы найти неотправленные, и если подобные задачи присутствуют – направляет их к клиентам.
  • Через определенный период времени (его можно настроить) сервер снова посылает сообщение всем клиентам, чтобы определить их номера и статусы. Это может быть полезно, когда новый клиент был запущен или остановлен уже после получения последнего запроса. Клиенты, которые не отвечают продолжительное время, помечаются как не отвечающие (подобные клиенты не участвуют в конвертации до тех пор, пока сервер не получит от них отклика, если задача назначается такому клиенту, то она перенаправляется другому). Клиент также может дать информацию о том, что в настоящий момент занят (с указанием задачи, которой он сейчас занят) или свободен.
  • Такой подход, когда клиент сообщает серверу о своем текущем статусе позволяет нам добавлять и удалять клиентов, не прерывая процесс, обеспечивая дополнительную возможность масштабировать систему.
  • Клиенты довольно пассивны в процессе коммуникации. Они только посылают сообщение в начале и завершении конвертации, и сообщение о своем статусе (свободен/занят).
  • Балансировка нагрузки в системе довольно проста. Поскольку мы используем FFmpeg для конвертации, мы совсем не можем повлиять на процесс конвертации, поэтому мы только выбираем первого свободного клиента и назначаем ему задачу. Если свободных серверов нет, задача ожидает до тех пор, пока новый клиент не запустится (клиент будет добавлен в сервер автоматически) или пока один из существующих серверов освободится.

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

  • Video Conversion Info. Этот объект для передачи данных содержит информацию о конвертации, включающую порядковый номер исходных и конвертированных видео ресурсов, а также тип хранения для того, чтобы система конвертирования могла правильно выбрать соединение с хранилищем. Данный объект создается внешней системой. Он распределяет все ресурсы и посылает их диспетчеру.
  • Перечень событий, включая Video Conversion Started Event (генерируется, когда клиент начинает конвертацию), Video Conversion Finished Event (запускается, когда клиент заканчивает конвертацию), Video Conversion Requested Event (запускается внешней системой, когда необходимо провести конвертацию) и Video Conversion Task Accepted Event (запускается диспетчером, когда задание получено и поставлено в очередь).
  • Также для конечных пользователей предлагается интерфейс Resource Storage Connector, необходимый для реализации различных вариантов хранения данных. В нашей системе мы используем Amazon S3.

Заключение

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

 

Поддержка пользовательских полей в Hibernate

 

При реализации бизнес-приложений уровня корпорации (Enterprise Scale) очень часто заказчик требует реализовать поддержку расширяемости (extensibility) объектной модели приложения без модификации исходного кода системы. Использование расширяемой объектной модели (Extensible Domain Model) позволяет без дополнительных усилий и затрат на разработку новой функциональности:

1) использовать приложение более длительный срок
2) модифицировать процесс работы системы с течением времени и при изменении внешних обстоятельств
3) «подстраивать» приложение под особенности работы предприятия, на котором оно внедрено.

Самым простым и дешевым способом достижения требуемой функциональности является реализация в приложении расширяемых бизнес сущностей (Extensible Business Entities) с поддержкой пользовательских полей (Custom Fields).

 

Что такое «пользовательское поле»?

Что такое «пользовательское поле» и что оно дает конечному пользователю системы?
«Пользовательское поле» – это атрибут объекта, который не был создан разработчиком системы на этапе разработки системы, а был добавлен пользователем системы в этот объект в ходе эксплуатации системы, без внесения изменений в исходный кол приложения.

Как такая функциональность может быть востребована?

Попробуем разобраться в этом на примере предметной области CRM. Предположим, что у нас в системе есть объект «Клиент». Теоретически у данного объекта может быть сколь угодно много различных атрибутов: несколько email-ов, несколько различных видов телефонов, различные адреса, и т.п. Одни из них могут использоваться сотрудниками отдела продаж одной фирмы, но могут совершенно не использоваться в другой организации. Внести в объект все возможные атрибуты, которые могут (а могут и не) использоваться конечными пользователями системы, по крайней мере, расточительно и мало оправдано.

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

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

Постановка задачи

В ходе реализации проекта Enterra CRM заказчиком была поставлена задача поддержки приложением функции Custom Fields в следующей формулировке:

«Администратор системы должен иметь возможность создания/удаления пользовательского поля без необходимости рестарта системы».

Для разработки бэк-энда системы был использован фрэймворк Hibernate 3.0. Это обстоятельство (технологическое ограничение) стало ключевым, с учетом которого было реализовано данное требование.

Техническое решение

В данном разделе мы приведем только ключевые моменты реализации с учетом особенностей использования фрэймворка Hibernate.

Среда

Приведенный демонстрационный вариант реализации разрабатывался с использованием:
1) JDK 1.5;
2) фрэймоворка Hibernate 3.2.0;
3) MySQL 4.1.

Ограничения

Для простоты мы не будем использовать Hibernate EntityManager, Hibernate Annotations. Маппинг персистентных объектов построим на основе xml mapping файлов. Более того, стоит отметить, что при использовании Hibernate Annotations приведенный вариант реализации не будет работоспособен, т.к. он основывается на управлении xml mapping файлами.

Задача

Наша задача реализовать механизм позволяющий создать/удалить пользовательское поле в режиме реального времени, без перезагрузки приложения, добавить в него какое-нибудь значение и убедиться, что данное значение отражено в базе данных приложения. Более того, нам необходимо убедиться, что мы можем использовать данное пользовательское поле в запросах.

Решение

Доменная модель

Начнем с того, что нам понадобится класс бизнес сущности, над которым мы будем производить эксперименты. Пусть это будет класс Contact. В нем будет два персистентных поля: id и name.
Но кроме этих постоянных и неизменных полей, данный класс должен предоставлять некоторую конструкцию для хранения значений пользовательских полей. Идеальной конструкцией для этих целей является Map.
Сделаем общий базовый класс для всех бизнес сущностей поддерживающих пользовательские поля – СustomizableEntity, который содержит Map CustomProperties для работы с пользовательскими полями:

package com.enterra.customfieldsdemo.domain; import java.util.Map; import java.util.HashMap; public abstract class CustomizableEntity { private Map customProperties; public Map getCustomProperties() { if (customProperties == null) customProperties = new HashMap(); return customProperties; } public void setCustomProperties(Map customProperties) { this.customProperties = customProperties; } public Object getValueOfCustomField(String name) { return getCustomProperties().get(name); } public void setValueOfCustomField(String name, Object value) { getCustomProperties().put(name, value); } }

Листинг 1 – базовый класс CustomizableEntity

Унаследуем наш класс Contact от этого базового класса:

package com.enterra.customfieldsdemo.domain; import com.enterra.customfieldsdemo.domain.CustomizableEntity; public class Contact extends CustomizableEntity { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }

Листинг 2 – Класс Contact унаследованный от CustomizableEntity.

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

<?xml version=»1.0″ encoding=»UTF-8″?> <!DOCTYPE hibernate-mapping PUBLIC «-//Hibernate/Hibernate Configuration DTD//EN» «http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd»> <hibernate-mapping auto-import=»true» default-access=»property» default-cascade=»none» default-lazy=»true»> <class abstract=»false» name=»com.enterra.customfieldsdemo.domain.Contact» table=»tbl_contact»> <id column=»fld_id» name=»id»> <generator class=»native»/> </id> <property name=»name» column=»fld_name» type=»string»/> <dynamic-component insert=»true» name=»customProperties» optimistic-lock=»true» unique=»false» update=»true»> </dynamic-component> </class> </hibernate-mapping>

Листинг 3 – Маппинг класс Contact.
На что стоит обратить внимание, что свойства id и name – оформляются как и все обычные свойства, а вот для customProperties мы используем тэг . Документация по Hibernate 3.2.0GA говорит нам, что назначение dynamic-component состоит в:

“The semantics of a <dynamic-component> mapping are identical to <component>. The advantage of this kind of mapping is the ability to determine the actual properties of the bean at deployment time, just by editing the mapping document. Runtime manipulation of the mapping document is also possible, using a DOM parser. Even better, you can access (and change) Hibernate’s configuration-time metamodel via the Configuration object.”

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

HibernateUtil и hibernate.cfg.xml

После того как мы определились с доменной моделью нашего приложения нам необходимо создать необходимые условия для функционирования фрэймворка Hibernate. Для этого необходимо создать конфигурационный файл hibernate.cfg.xml и класс для работы с базовыми функциями Hibernate.

<?xml version=’1.0′ encoding=’utf-8′?> <!DOCTYPE hibernate-configuration PUBLIC «-//Hibernate/Hibernate Configuration DTD//EN» «http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd»> <hibernate-configuration> <session-factory> <property name=»show_sql»>true</property> <property name=»dialect»>org.hibernate.dialect.MySQLDialect</property> <property name=»cglib.use_reflection_optimizer»>true</property> <property name=»hibernate.connection.driver_class»>com.mysql.jdbc.Driver</property> <property name=»hibernate.connection.url»>jdbc:mysql://localhost:3306/custom_fields_test</property> <property name=»hibernate.connection.username»>root</property> <property name=»hibernate.connection.password»></property> <property name=»hibernate.c3p0.max_size»>50</property> <property name=»hibernate.c3p0.min_size»>0</property> <property name=»hibernate.c3p0.timeout»>120</property> <property name=»hibernate.c3p0.max_statements»>100</property> <property name=»hibernate.c3p0.idle_test_period»>0</property> <property name=»hibernate.c3p0.acquire_increment»>2</property> <property name=»hibernate.jdbc.batch_size»>20</property> <property name=»hibernate.hbm2ddl.auto»>update</property> </session-factory> </hibernate-configuration>

Листинг 4 – конфигурационный файл Hibernate.

Файл hibernate.cfg.xml не содержит ничего примечательного кроме, пожалуй, строчки:

<property name=»hibernate.hbm2ddl.auto»>update</property>

Листинг 5 – использование авто-апдейта.

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

package com.enterra.customfieldsdemo; import org.hibernate.*; import org.hibernate.mapping.PersistentClass; import org.hibernate.tool.hbm2ddl.SchemaUpdate; import org.hibernate.cfg.Configuration; import com.enterra.customfieldsdemo.domain.Contact; public class HibernateUtil { private static HibernateUtil instance; private Configuration configuration; private SessionFactory sessionFactory; private Session session; public synchronized static HibernateUtil getInstance() { if (instance == null) { instance = new HibernateUtil(); } return instance; } private synchronized SessionFactory getSessionFactory() { if (sessionFactory == null) { sessionFactory = getConfiguration().buildSessionFactory(); } return sessionFactory; } public synchronized Session getCurrentSession() { if (session == null) { session = getSessionFactory().openSession(); session.setFlushMode(FlushMode.COMMIT); System.out.println(«session opened.»); } return session; } private synchronized Configuration getConfiguration() { if (configuration == null) { System.out.print(«configuring Hibernate … «); try { configuration = new Configuration().configure(); configuration.addClass(Contact.class); System.out.println(«ok»); } catch (HibernateException e) { System.out.println(«failure»); e.printStackTrace(); } } return configuration; } public void reset() { Session session = getCurrentSession(); if (session != null) { session.flush(); if (session.isOpen()) { System.out.print(«closing session … «); session.close(); System.out.println(«ok»); } } SessionFactory sf = getSessionFactory(); if (sf != null) { System.out.print(«closing session factory … «); sf.close(); System.out.println(«ok»); } this.configuration = null; this.sessionFactory = null; this.session = null; } public PersistentClass getClassMapping(Class entityClass){ return getConfiguration().getClassMapping(entityClass.getName()); } }

Листинг 6 –HibernateUtils класс.
Наряду с обычными методами, такими как getCurrentSession(), getConfiguration(), который необходим для обычной работы приложения на базе Hibernate, мы реализовали такие методы как: reset() и getClassMapping(Class entityClass).
В методе getConfiguration(), мы производим конфигурирование Hibernate и добавляем класс Contact в конфигурацию.
Метод reset() используется для закрытия всех используемых Hibernate ресурсов и сброса всех его настроек:

public void reset() { Session session = getCurrentSession(); if (session != null) { session.flush(); if (session.isOpen()) { System.out.print(«closing session … «); session.close(); System.out.println(«ok»); } } SessionFactory sf = getSessionFactory(); if (sf != null) { System.out.print(«closing session factory … «); sf.close(); System.out.println(«ok»); } this.configuration = null; this.sessionFactory = null; this.session = null; }

Листинг 7 – метод reset()
Метод getClassMapping(Class entityClass) возвращает объект PersistentClass, который содержит полную информацию о маппинге переданной ему сущности. Именно манипуляции с объектом PersistentClass и позволяют модифицировать состав атрибутов класса сущности в режиме run-time.

public PersistentClass getClassMapping(Class entityClass){ return getConfiguration().getClassMapping(entityClass.getName()); }

Листинг 8 – метод getClassMapping(Class entityClass).

Манипуляции с маппингом

После того как у нас есть класс бизнес сущности (Contact) и основной класс для взаимодействия с Hibernate мы можем начинать работу. Мы можем создавать и сохранять экземпляры класса Contact. Мы можем даже помещать какие-нибудь данные в наш Map customProperties, но мы должны знать, что эти данные (хранящиеся в Map-е customProperties) не сохранятся в БД.
Для того, чтобы они сохранились, нам надо обеспечить механизм создания пользовательских полей в нашем классе, и сделать так, чтобы Hibernate научился с ними работать.
Для того чтобы обеспечить возможность манипуляций маппингом класса мы должны создать некоторый интерфейс для этого. Назовем его CustomizableEntityManager. Его название должно отражать назначение данного интерфейса – управление бизнес сущностью, ее содержимым, ее атрибутами:

package com.enterra.customfieldsdemo; import org.hibernate.mapping.Component; public interface CustomizableEntityManager { public static String CUSTOM_COMPONENT_NAME = «customProperties»; void addCustomField(String name); void removeCustomField(String name); Component getCustomProperties(); Class getEntityClass(); }

Листинг 9 – Интерфейс CustomizableEntityManager
Основными методами данного интерфейса являются естественно: void addCustomField(String name) и void removeCustomField(String name). Которые должны создать и удалить наше пользовательское поле в маппинге соотвествующего класса.
Ниже приводится реализация данного интерфейса:

package com.enterra.customfieldsdemo; import org.hibernate.cfg.Configuration; import org.hibernate.mapping.*; import java.util.Iterator; public class CustomizableEntityManagerImpl implements CustomizableEntityManager { private Component customProperties; private Class entityClass; public CustomizableEntityManagerImpl(Class entityClass) { this.entityClass = entityClass; } public Class getEntityClass() { return entityClass; } public Component getCustomProperties() { if (customProperties == null) { Property property = getPersistentClass().getProperty(CUSTOM_COMPONENT_NAME); customProperties = (Component) property.getValue(); } return customProperties; } public void addCustomField(String name) { SimpleValue simpleValue = new SimpleValue(); simpleValue.addColumn(new Column(«fld_» + name)); simpleValue.setTypeName(String.class.getName()); PersistentClass persistentClass = getPersistentClass(); simpleValue.setTable(persistentClass.getTable()); Property property = new Property(); property.setName(name); property.setValue(simpleValue); getCustomProperties().addProperty(property); updateMapping(); } public void removeCustomField(String name) { Iterator propertyIterator = customProperties.getPropertyIterator(); while (propertyIterator.hasNext()) { Property property = (Property) propertyIterator.next(); if (property.getName().equals(name)) { propertyIterator.remove(); updateMapping(); return; } } } private synchronized void updateMapping() { MappingManager.updateClassMapping(this); HibernateUtil.getInstance().reset(); // updateDBSchema(); } private PersistentClass getPersistentClass() { return HibernateUtil.getInstance().getClassMapping(this.entityClass); } }

Листинг 10 – реализация интерфейса CustomizableEntityManager
Прежде всего, надо отметить, что при создании класса CustomizableEntityManager мы указываем каким собственно классом бизнес-сущности будет управлять этот менеджер. Этот класс передается в качестве параметра конструктору CustomizableEntityManager:

private Class entityClass; public CustomizableEntityManagerImpl(Class entityClass) { this.entityClass = entityClass; } public Class getEntityClass() { return entityClass; }

Листинг 11 – конструктор класса CustomizableEntityManagerImpl
Теперь нас больше всего интересует реализация метода void addCustomField(String name) :

public void addCustomField(String name) { SimpleValue simpleValue = new SimpleValue(); simpleValue.addColumn(new Column(«fld_» + name)); simpleValue.setTypeName(String.class.getName()); PersistentClass persistentClass = getPersistentClass(); simpleValue.setTable(persistentClass.getTable()); Property property = new Property(); property.setName(name); property.setValue(simpleValue); getCustomProperties().addProperty(property); updateMapping(); }

Листинг 12 – Создание пользовательского поля.
Как видим из реализации, Hibernate предоставляет большие возможности по работе со свойствами персистентных объектов и их отображением в БД. По сути данного метода:
1) Мы создаем объект класс SimpleValue, которые позволяет нам указать, каким образом будет храниться значение данного пользовательского поля в БД – в каком поле и в какой таблице БД:

SimpleValue simpleValue = new SimpleValue(); simpleValue.addColumn(new Column(«fld_» + name)); simpleValue.setTypeName(String.class.getName()); PersistentClass persistentClass = getPersistentClass(); simpleValue.setTable(persistentClass.getTable());

Листинг 13 – создание колонки таблицы.

2) Создаем свойство персистентного объекта и добавляем его в динамический компонент (!), который мы и планировали использовать для данной цели:

Property property = new Property() property.setName(name) property.setValue(simpleValue) getCustomProperties().addProperty(property)

Листинг 14 – создание свойства объекта.

3) Последнее что нам нужно сделать – это сделать так, чтобы наше приложение произвело соответствующие изменения в xml файлах и обновило конфигурацию Hibernate. Делается это методом updateMapping();
Необходимо прояснить назначение еще двух методов вызовы, которых производились в описанном выше коде. Первый метод – это getCustomProperties():

public Component getCustomProperties() { if (customProperties == null) { Property property = getPersistentClass().getProperty(CUSTOM_COMPONENT_NAME); customProperties = (Component) property.getValue(); } return customProperties; }

Листинг 15 – получение CustomProperties как Component.
Данный метод находи и возвращает объект Component соответствующий тэгу в маппинге нашей бизнес-сущности.
Второй метод это updateMapping():

private synchronized void updateMapping() { MappingManager.updateClassMapping(this); HibernateUtil.getInstance().reset(); // updateDBSchema(); }

Листинг 16 – метод updateMapping().
Он отвечает за сохранение измененного маппинга персистентного класса, и сбрасывает состояние конфигурации Hibernate, для того, чтобы при следующем обращении изменения, внесенные нами, вступили в силу.
Кстати, тут стоит вернуться к строке:
<property name=”hibernate.hbm2ddl.auto”>update</property>
конфигурации Hibernate. Если бы этой строки не было, то нам необходимо было бы еще и запустить обновление схемы БД средствами Hibernate. Но использование указанной настройки позволяет нам не делать этого.

Сохранение маппинга

Модификации маппинга произведенные в run-time не сохраняются сами по себе в соответствующий xml файл маппинга, и для того, чтобы внесенные изменения вступили в силу и при следующем запуске приложения, нам необходимо самостоятельно (врусную) сохранить изменения в соответствующий маппинг файл.
Для этого мы будем использовать класс MappingManager основная задача которого – сохранение маппинга указанной бизнес-сущности в ее xml маппинг-файл:

package com.enterra.customfieldsdemo; import com.enterra.customfieldsdemo.domain.CustomizableEntity; import org.hibernate.Session; import org.hibernate.mapping.Column; import org.hibernate.mapping.Property; import org.hibernate.type.Type; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.Iterator; public class MappingManager { public static void updateClassMapping(CustomizableEntityManager entityManager) { try { Session session = HibernateUtil.getInstance().getCurrentSession(); Class<? extends CustomizableEntity> entityClass = entityManager.getEntityClass(); String file = entityClass.getResource(entityClass.getSimpleName() + «.hbm.xml»).getPath(); Document document = XMLUtil.loadDocument(file); NodeList componentTags = document.getElementsByTagName(«dynamic-component»); Node node = componentTags.item(0); XMLUtil.removeChildren(node); Iterator propertyIterator = entityManager.getCustomProperties().getPropertyIterator(); while (propertyIterator.hasNext()) { Property property = (Property) propertyIterator.next(); Element element = createPropertyElement(document, property); node.appendChild(element); } XMLUtil.saveDocument(document, file); } catch (Exception e) { e.printStackTrace(); } } private static Element createPropertyElement(Document document, Property property) { Element element = document.createElement(«property»); Type type = property.getType(); element.setAttribute(«name», property.getName()); element.setAttribute(«column», ((Column) property.getColumnIterator().next()).getName()); element.setAttribute(«type», type.getReturnedClass().getName()); element.setAttribute(«not-null», String.valueOf(false)); return element; } }

Листинг 17 – утилита для обновления Маппинга персистентного класса.
Данный класс делает буквально следующее:
1) Определяет местонахождение и загружает xml маппинг для указанной бизнес-сущности в объект DOM Document для последующих манипуляций с ним;
2) Находит элемент данного документ . Именно в нем мы храним пользовательские поля и именно его содержимое меняем;
3) Удаляем (!) все вложенные элементы из данного элемента;
4) Для каждого персистентного свойства (Property) содержащегося в нашем Компоненте, отвечающем за хранение пользовательских полей, создаем соответствующий элемент документа и устанавливаем этому элементу атрибуты из соответствующего объекта Property;
5) Сохраняем этот вновь созданный файл маппинга.
При манипуляциях с XML мы используем (как видно из кода) класс XMLUtil, который в принципе может быть реализован как угодно, лишь бы правильно загружал и сохранял xml файл.
Наша реализация приведена на следующем листинге:

package com.enterra.customfieldsdemo; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Document; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilder; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.Transformer; import javax.xml.transform.OutputKeys; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.dom.DOMSource; import java.io.IOException; import java.io.FileOutputStream; public class XMLUtil { public static void removeChildren(Node node) { NodeList childNodes = node.getChildNodes(); int length = childNodes.getLength(); for (int i = length — 1; i > -1; i—) node.removeChild(childNodes.item(i)); } public static Document loadDocument(String file) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.parse(file); } public static void saveDocument(Document dom, String file) throws TransformerException, IOException { TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, dom.getDoctype().getPublicId()); transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, dom.getDoctype().getSystemId()); DOMSource source = new DOMSource(dom); StreamResult result = new StreamResult(); FileOutputStream outputStream = new FileOutputStream(file); result.setOutputStream(outputStream); transformer.transform(source, result); outputStream.flush(); outputStream.close(); } }

Листинг 18 – утилита для манипуляций с XML.

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

Теперь, когда весь необходимый нам рабочий код написан, мы можем написать тесты и посмотреть как же это все работает.
Первый тест будет создавать пользовательское поле email, создавать и сохранять объект класса контакт и устанавливать ему свойство email.
Для начала посмотрим на таблицу данных tbl_contact. Она содержит два поля: fld_id, fld_name.
Код теста приведен ниже:

package com.enterra.customfieldsdemo.test; import com.enterra.customfieldsdemo.HibernateUtil; import com.enterra.customfieldsdemo.CustomizableEntityManager; import com.enterra.customfieldsdemo.CustomizableEntityManagerImpl; import com.enterra.customfieldsdemo.domain.Contact; import org.hibernate.Session; import org.hibernate.Transaction; import java.io.Serializable; public class TestCustomEntities { private static final String TEST_FIELD_NAME = «email»; private static final String TEST_VALUE = «[email protected]«; public static void main(String[] args) { HibernateUtil.getInstance().getCurrentSession(); CustomizableEntityManager contactEntityManager = new CustomizableEntityManagerImpl(Contact.class); contactEntityManager.addCustomField(TEST_FIELD_NAME); Session session = HibernateUtil.getInstance().getCurrentSession(); Transaction tx = session.beginTransaction(); try { Contact contact = new Contact(); contact.setName(«Contact Name 1»); contact.setValueOfCustomField(TEST_FIELD_NAME, TEST_VALUE); Serializable id = session.save(contact); tx.commit(); contact = (Contact) session.get(Contact.class, id); Object value = contact.getValueOfCustomField(TEST_FIELD_NAME); System.out.println(«value = » + value); } catch (Exception e) { tx.rollback(); System.out.println(«e = » + e); } } }

Листинг 19 – тест создания пользовательского поля.
Данный метод делает следующее:
1) Создает CustomizableEntityManager для класса Contact;
2) Создает новое пользовательское поле в нем под названием email;
3) Затем в транзакции создаем новый контакт и присваеваем пользовательскому полю значение “[email protected]”;
4) Сохраняем Контакт;
5) Получаем значение пользовательского поля “email”.
В результате выполнения видим следующее:

configuring Hibernate … ok session opened. closing session … ok closing session factory … ok configuring Hibernate … ok session opened. Hibernate: insert into tbl_contact (fld_name, fld_email) values (?, ?) value = [email protected]

Листинг 20 – результат теста.
В базе данных видим следующую запись:

+———+———————+———————-+ | fld_id | fld_name | fld_email | +———+———————+———————-+ | 1 | Contact Name 1 | [email protected] | +———+———————+———————-+

Листинг 21 – Результат в БД.
Как мы видим новое поле создано в режиме run-time и значение этого поля успешно сохранено.
Второй тест производит запрос к БД с использованием вновь созданного поля:

package com.enterra.customfieldsdemo.test; import com.enterra.customfieldsdemo.HibernateUtil; import com.enterra.customfieldsdemo.CustomizableEntityManager; import com.enterra.customfieldsdemo.domain.Contact; import org.hibernate.Session; import org.hibernate.Criteria; import org.hibernate.criterion.Restrictions; import java.util.List; public class TestQueryCustomFields { public static void main(String[] args) { Session session = HibernateUtil.getInstance().getCurrentSession(); Criteria criteria = session.createCriteria(Contact.class); criteria.add(Restrictions.eq(CustomizableEntityManager.CUSTOM_COMPONENT_NAME + «.email», «[email protected]«)); List list = criteria.list(); System.out.println(«list.size() = » + list.size()); } }

Листинг 22 – Тест запроса по пользовательскому полю.
Результат выполнения:

Execution result: configuring Hibernate … ok session opened. Hibernate: select this_.fld_id as fld1_0_0_, this_.fld_name as fld2_0_0_, this_.fld_email as fld3_0_0_ from tbl_contact this_ where this_.fld_email=? list.size() = 1

Листинг 23 – Результат выполнения запроса.
Как мы видим пользовательское поле, созданное по нашей технологии, очень легко участвует в запросах к БД.

Дальнейшее развитие идеи

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

Очевидно также, что данное требование может быть реализовано с использованием другим механизмов (например, кодогенерация), которые возможно будут рассмотрены в других статьях.

Данная реализация поддерживает только тип String в качестве Custom Fields, в реальном же приложении, построенном на базе этого подхода (Enterra CRM), была реализована полная поддержка, как всех примитивных типов, так и объектных типов (ссылок на бизнес-объекты) и коллекционных полей.

Для поддержки пользовательских полей в части пользовательского интерфейса была реализована система мета-описателей пользовательских полей, которые использовались системой генерации пользовательского интерфейса. Но механизм работы данного генератора – это тема для отдельной статьи.

Заключение

В результате работы команды проекта Enterra CRM была разработана, апробирована и применена на практике архитектура расширяемой объектной модели на базе ORM платформы Hibernate, которая позволила удовлетворить потребности заказчика в плане настройки приложения под потребности конечных пользователей в режиме эксплуатации системы, без необходимости внесения изменений в исходный код приложения.