Показаны сообщения с ярлыком Groovy. Показать все сообщения
Показаны сообщения с ярлыком Groovy. Показать все сообщения

четверг, 28 марта 2013 г.

Создаем свой язык в Groovy

Основная проблема императивных языков программирования - их низкая приближенность к естественным языкам.

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

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

// Клиент
class Customer {
    int inn
    String name
    String address
    String phone
}

// Клиенты
class Customers {
    Customer findByInn(inn)
    void add(Customer customer)
}

// Продукт
class Product {
    String article
    String name
    double price
}

// Продукты
class Products {
    Product findByArticle(article)
    void add(Product product)
}

// Заказ
class Order {
    int num
    Customer customer
    List< orderdetail> details = []

    OrderDetail findByPos(pos)
    void add(OrderDetail detail)
}

// Товар заказа
class OrderDetail {
    int pos
    Product product
    def count = 1
    def getSum() { count * product.price }
}

// Заказы
class Orders {
    Order findByNum(num)
    void add(Order order)
}

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

// Списки бизнес-сущностей
def customers = new Customers()
def products = new Products()
def orders = new Orders()

// Добавление клиента
customers.add(new Customer(inn: 1234, name: "Клиент", address: "Россия", 
      phone: "+74951002030"))

// Добавление продукта
products.add(new Product(article: "a100", name: "Товар 1", price: 100.00))
products.add(new Product(article: "a200", name: "Товар 2", price: 200.00))

// Добавление заказа
def order = new Order(num: 1, customer: customers.findByInn(1234))
order.add(new OrderDetail(pos: 1, 
   product: products.findByArticle("a100"), count: 1))
order.add(new OrderDetail(pos: 2, 
   product: products.findByArticle("a200"), count: 1))
orders.add(order)

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

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

AddCustomer(inn: 1234, name: "Клиент", address: "Россия", phone: "+74951002030")

AddProduct(article: "a100", name: "Товар 1", price: 100.00)
AddProduct(article: "a200", name: "Товар 2", price: 200.00)

AddOrder(num: 1, customer: 1234) {
 Detail(pos: 1, product: "a100", count: 1)
 Detail(pos: 2, product: "a200", count: 1)
}

Такой код вообще не нуждается в комментариях - он имеет высокую читабельность.
Для реализации этого языка потребуется написать билдер на Groovy. У Groovy есть абстрактный класс BuilderSupport, от которого нужно наследоваться, чтобы создать свой билдер. В наследуемом классе потребуется перекрыть ряд методов, которые Groovy будет автоматически вызывать при разборе языка разметки в коде. Вот как будет выглядеть класс билдера:

public class MyBuilder extends BuilderSupport {
 public Customers customers
 public Products products
 public Orders orders

 // Назначение ноде родительской ноды
 protected void setParent(Object parent, Object child) {
 }

 // Создать ноду без параметров
 protected Object createNode(Object name) {
  if (name != "call") 
   throw new Exception("Node required parameters")
  new Node(null, name);
 }

 // Создать ноду с привязанным к ней объектом
 protected Object createNode(Object name, Object value) {
  throw new Exception("Node required parameters")
 }

 // Создать ноду с параметрами
 protected Object createNode(Object name, Map attributes) {
  // Получаем родительскую текущую ноду
  Node parent = getCurrent()
  def result
  
  // Анализируем имя ноды
  switch (name) {
   case "AddCustomer":
    result = addCustomer(attributes)
    break
   case "AddProduct":
    result = addProduct(attributes)
    break
   case "AddOrder":
    result = addOrder(attributes)
    break
   case "Detail":
    if (parent == null || 
     parent.name() != "AddOrder")
     throw new Exception(
      "Detail must be specified with only AddOrder")

    result = addOrderDetail(parent.value(), attributes)
    break
   defailt:
    throw new Exception("Unknown node ${name}")
  }
  new Node(null, name, attributes, result);
 }

 // Создать ноду с параметрами и привязанным к ней объектом
 protected Object createNode(Object name, Map attributes, Object value) {
  throw new Exception("Node ${name} can not support objects")
 }

 // Добавляем клиента
 def addCustomer(Map params) {
  def customer = new Customer(inn: params.inn, name: params.name, 
      address: params.address, phone: params.phone)
  customers.add(customer)
  println "Added customer ${customer.inn}: ${customer.name}"
  customer
 }
 
 // Добавляем продукт
 def addProduct(Map params) {
  def product = new Product(article: params.article, name: params.name, 
      price: params.price)
  products.add(product)
  println "Added product ${product.article}: ${product.name}"
  product
 }
 
 // Добавляем заказ
 def addOrder(Map params) {
  def order = new Order(num: 1, customer: customers.findByInn(params.customer))
  orders.add(order)
  println "Added order ${order.num} from customer ${order.customer.name}"
  order
 }
 
 // Добавляем строку заказа
 def addOrderDetail(Order order, Map params) {
  def count = params.count?:1
  def detail = new OrderDetail(pos: params.pos, 
      product: products.findByArticle(params.product), 
      count: count)
  order.add(detail)
  println "Added into order ${order.num} detail pos ${detail.pos} " +
    "with product ${detail.product.name}"
  detail
 }
}

В этом классе перекрыто два абстрактных метода setParent и createNode. setParent вызывается при назначении дочерней ноде родителя и в моей логике не используется. А вот в createNode как раз и вызывается на каждый элемент разметки. В зависимости от синтаксиса описания ноды разметки, вызывается один из четырех перегруженных методов createNode. Моей синтаксис предполагает, что у элементов всегда есть параметры. Поэтому я прописал необходимую функциональность в нужный метод и добавил исключения во все остальные методы createNode. Это позволит проконтролировать и исключить неправильный синтаксис описания вызова методов. Единственное исключение было сделано для рутовой метки call, которая автоматически создается первой при запуске билдера без параметров. Класс билдера я расширил конструктором, в который передаются созданные объекты списков клиентов, продуктов и заказов. Так же описал в классе методы добавления бизнес сущностей. Ничего сложного - все замечательно видно в коде и по комментариям в нем.

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

// Списки бизнес-сущностей
def customers = new Customers()
def products = new Products()
def orders = new Orders()

// Создать объект билдера
def myApi = new MyBuilder(customers: customers, products: products, orders: orders)

// Вызвать билдер
myApi {
 AddCustomer(inn: 1234, name: "Клиент", address: "Россия", phone: "+74951002030")
 
 AddProduct(article: "a100", name: "Товар 1", price: 100.00)
 AddProduct(article: "a200", name: "Товар 2", price: 200.00)
 
 AddOrder(num: 1, customer: 1234) {
  Detail(pos: 1, product: "a100", count: 1)
  Detail(pos: 2, product: "a200", count: 1)
 }
}

// Результаты
println "\n*** Result ***"
println "Customers:"
println customers
println "Products:"
println products
println "Orders:"
println orders

Результат:
Added customer 1234: Клиент
Added product a100: Товар 1
Added product a200: Товар 2
Added order 1 from customer Клиент
Added into order 1 detail pos 1 with product Товар 1
Added into order 1 detail pos 2 with product Товар 2

*** Result ***
Customers:
{inn=1234, name=Клиент, address=Россия, phone=+74951002030}
Products:
{article=a100, name=Товар 1, price=100.0}
{article=a200, name=Товар 2, price=200.0}
Orders:
{num=1, customer=Клиент, 
 detail={pos=1, product=Товар 1, count=1, sum=100.0};
   {pos=2, product=Товар 2, count=1, sum=200.0}}

Все работает :)

Резюмируя можно сказать, что область применения у билдеров большая. Я, например, сейчас на его основе разрабатываю язык описания трансформации данных для своего open source проекта GETL (ETL на базе Groovy). С помощью билдера можно легко разработать синтаксис, который позволит собирать SQL запросы в коде или выводить информацию в иерархическом собственном форматированном виде. Да и штатные XML/JSON маркеры думаю теперь не представляют из себя тайны. Язык разметок можно использовать не только в коде программ на Groovy, но и как блоки описания объектов и действий, которые вынесены в отдельные файлы. Блоки описания можно прямо в runtime считывать из файлов и выполнять с помощью метода EVAL. Так как блоки хорошо формализованы, то для них можно легко написать собственный GUI разработки бизнес логики обычными пользователями.

Примеров можно привести множество. Но самое главное, не стоит забывать - все вышесказанное замечательно без каких либо усилий работает на Java! Никто не мешает в Groovy обвязать любые Java классы и методы своим языком разметки, на котором писать бизнес логику, которая дальше используется в Java приложениях. Эти возможности стоят того, чтобы начать использовать в Java волшебный Groovy :)

Полный код скриптов классов можно скачать отсюда.

вторник, 26 марта 2013 г.

Выложен билд GETL версия 1.0.1 альфа


Привет.

Двенадцать ударных дней (суток) с момента старта проекта принесли победный результат: GETL перестал быть набором абстрактных идей и недо-классов и сегодня был выложен как уже собранная JAR, готовый к использованию:
https://sourceforge.net/projects/getl/files/

Пусть пока и альфа версия, пусть пока в наличии есть Extract и Load, но нет Transform, но уже в базовом функционале уже многое есть, что можно использовать.

Итак на текущий момент эта версия GETL умеет:
  • Работать с CSV файлами: получать список файлов, удалять файлы, получать схемы полей файлов, читать и сохранять записи, осуществлять поддержку всех типов полей с автоматической конвертацией из текста файла в нужные типы, указывать форматирование значений полей для десятичных типов, даты, времени и булевого типа, проверять при чтении поля на заполнение и уникальность, считывать заданное кол-во записей из источника.
  • Работать с JDBC соединениями: получать список объектов БД, получать поля таблицы, считывать записи таблицы или запроса, сохранять записи в таблицы в пакетном режиме через PreparedStatement, считывать заданное кол-во записей с источника с заданного номера записи.
  • Организовывать многопоточный запуск процессов по указанным элементам списка для организации параллельной обработки данных.
  • Копировать данные из источника в приемник: автоматически маппировать поля источника и приемника по их именам, автоматически конвертировать значений полей источника в значения с учетом типов приемника, выполнять пользовательский код для преобразования и трансформации данных из записи источника в запись приемника.
  • Организовать процессинг обработки записей источника кодом пользователя.
  • Организовать процессинг сохранения записей в приемник, генерируемых кодом пользователя.
Для облегчения и универсализации управления источниками данных был разработан класс Dataset, который позволяет описать метаданные и атрибуты источника, содержит описание полей структуры источника. Имеется возможность указать для Dataset свой код обработки  и изменения схемы данных при ее получении при соединении, а так же код проверки записи при чтении данных. Dataset не привязан к типам соединений, его параметры можно сохранять и восстанавливать в конфигурационных файлах. Для удобства использования поверх Dataset созданы классы CSVDataset, TableDataset и QueryDataset, маппирующие основные параметры, присущие этим типам данных как свойства класса.

Так же для организации соединений разработан класс Connection, который так же не привязан к типам источников данных и универсален. Сам Connection при создании требует указать драйвер типа источника данных, на текущий момент в GETL поддерживаются драйвера CSVDriver и JDBCDriver. Для большего удобства использования поверх Connection созданы классы FileConnection и JDBCConnection.

Для работы с CSV файлами был использован open-source продукт SuperCSV. Он полностью покрыл собой работу с CSV файлами, показав отличную производительность и мне осталось только в CSVDriver прописать его использование. От себя я могу только сказать автору огромное большущее спасибо за гигантскую отлично проделанную работу и сказать, что open source это здорово - я себе не представляю даже, если бы я самостоятельно решил в GETL разработать такого уровня работу с CSV файлами, сколько времени и нервов на это бы ушло.

Для работы с JDBC все конечно проще - JDBC уже полностью покрывает собой базовый функционал работы с РСУБД, поэтому разработанные на его основе драйвер JDBCDriver будет теоретически работать с любым сервером, а практически конечно же это придется все постепенно проверять. Греет мысль, что JDBC достаточно строгий и все реализации под него драйверов более менее работают так, как заявлено в спецификации. В дальнейшем будет достаточно наследоваться от этого драйвера под конкретные СУБД и расширять уже под конкретные реализации серверов такую функциональность, как пакетная загрузка файла, создание таблицы, работа с CDC и прочее. Здесь больших трудностей не видно.

Для оптимизации работы использовались такие возможности Groovy, как динамическая компиляций кода на лету, о чем я писал ранее в блоге. Это используется сейчас в копировании данных при сборке автомаппинга с автоконвертацией, а так же при сохранении в JDBC источники через PreparedStatement с использованием пакетных операций.

Таким образом, базовый функционал есть, а для чтения и записи XML и JSON файлов, с учетом того, что они хранят данные в виде иерархий, логичнее всего использовать базовые возможности Groovy, очень мощные и легкие на уровне написания кода. Ну а записать результат чтения таких файлов в CSV/JDBC или же наоборот записать в XML/JSON записи из CSV/JDBC с помощью GETL занимает пару строчек кода и не требует усилий.

Так же в Groovy есть полная поддержка ANT, на котором можно писать управляемые сценарии работы. Здесь я тоже не вижу смысла писать собственные решения по работе с файлами, FTP или архиваторами, апачевский ANT всегда будет мощнее и круче того, что я напишу. Ну а благодаря Groovy, сценарии для ANT на нем писать легко и просто, поэтому такого рода функциональность считаю полностью охваченной штатными средствами.

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

В качестве небольшой демонстрации работы с GETL, на SourgeForge в файлах выложены примеры, которые можно запустить как консольные приложения через бат-файл, если установлен Groovy.

P.S. И да - весь код исходников GETL несмотря на то, что уже многое умеет, сейчас весит ... 74 килобайта. Конечно тут есть и моя заслуга - грамотное проектирование иерархий классов и архитектуры никто не отменял. Но в первую очередь это заслуга Groovy, к слову сказать если бы я это стал реализовывать на Java, объем исходного кода был бы больше на порядок в абсолютном понимании этого термина. Groovy это круто!

четверг, 14 марта 2013 г.

ETL на Groovy - альфа на горизонте


Привет.

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

В ходе разработки использовались многие интересные моменты Groovy. Самый впечатляющий из них - это динамическая кодогенерация с последующей компиляцией во время выполнения программного кода. Это позволило реализовать такую мощную фишку, как генерация кода трансформации данных на лету. На момент запуска потока движения данных GETL по метаданным источников данных и правилам маппинга и трансформации данных генерирует на Groovy код логики работы, который тут же на лету компилируется в байт код Java и вызывается в процессе движения и преобразования данных. Так как на выходе получается код,  оперирующий напрямую нужными полями и функциями, без проверок и дополнительных операций, это позволяет добиться отличной скорости обработки данных.

Возможность кодогенерации есть и у других ETL. Например, Talend так же генерирует по описанию job код на Java, который уже максимально ориентирован и оптимизирован под выполнение описанных задач. Однако здесь самый важный нюанс: GETL, в отличие от Talend, и других кодогенерирующих ETL является полностью динамическим. Прямо по ходу его работы можно описывать новые структуры или изменять старые и по произведенным изменениям будет и меняться генерирующийся код обработки данных. В Talend это не возможно - если структура источника данных не совпадает с описанной структурой в job, то необходимо изменять этот job в дизайнере и компилировать его перед запуском. Если же структура источника плавающая, то придется отказаться от штатной функциональности Talend и вручную писать собственный код на Java для обработки таких источников.

Ну а если сказать коротко, то GETL в отличие от других кодогенерирующих ETL, позволит создавать шаблоны для job-ов и использовать их для типовой работы над множеством источников с разной структурой данных. Это на порядки сократит разработку загрузчиков данных. И все это без потери скорости обработки данных!

На текущий момент ведется разработка поддержки GETL источников данных JDBC, XML и JSON. После этого будет собран и выложен альфа релиз продукта, а так же опубликован Road Map дальнейшего развития продукта.

До связи!

суббота, 16 февраля 2013 г.

Проект GETL


Привет.

Долго я вынашивал идею разработки open source ETL и вот эта идея стала приобретать очертания. Так зачем же еще один open source ETL? (про коммерческие молчу, еще один там явно не нужен). По моему мнению, все известные мне бесплатные и платные ETL продукты хромают в той или иной мере по трем позициям:

1.    ETL продукты чрезвычайно автономны и самодостаточны. На практике это означает то, что вам очень сложно управлять ETL продуктом из собственной программы и такое управление ограничено возможностями API этого ETL. Так же не менее сложным является использование в ETL Ваших и сторонних решений, приходится писать тучу кода и обвязок, чтобы их подружить. Все это касается и интеграции с системами автоматизированного тестирования. Конечно, современные ETL продукты в том или ином виде поддерживают тестирование, но максимально только на уровне работы самого job. Провести комплексное тестирование частей job или множества job для ETL по заранее заданному плану, интересная, но сложная задача для тестировщика.
2.    ETL продукты очень консервативны по отношению к структурам данных. Они любят заранее определенные структуры данных и очень слабо работают с данными, схемы которых не известны заранее, представляя для работы с ними достаточно ограниченный функционал и накладывая множество ограничений. Ну, а разработка шаблонного job, который производил бы типовые действия над структурами неизвестными на момент разработки это вообще из области фантастики.
3.    Даруя простоту разработки в виде визуальных диаграмм, ETL продукты теряют простоту реализации сложных алгоритмов обработки данных. Ветвление, итерации, сложная многоступенчатая трансформация данных - все это выливается на диаграммах в огромные нечитабельные схемы, внутри которых компоненты диаграмм все равно содержат выражения и алгоритмический код. Приходится много времени тратить на то, чтобы все эти условия и циклы просто правильно соединить на схеме, разбивать на подчиненные задачи, описывая их вызов и передачу параметров, уводить на диаграмме какие-то особенно сложные вещи в свой код.
Сейчас все основные работы по загрузке данных в хранилище я разрабатываю на Talend DataIntegrator. Этот продукт был выбран как наиболее универсальный и позволяющий легко расширять себя до необходимого функционала с помощью разработки собственных библиотек и компонент на Java. Talend имеет внушительный функционал по поддержке различных источников данных и методам работы с данными. Являясь кодогенератором, Talend генерирует по разработанным job код на Java, что обеспечивает отличную скорость обработки данных. Также этот сгенерированный код в виде классов Java можно использовать внутри собственных приложений или контейнера J2EE.

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

Часто получалось, что задачу не представляется возможным реализовать в виде одного job и приходилось часть действий выносить в подчиненные job, вызываемые с основного. Это размазывало логику загрузки и усложняло решение из-за организации взаимодействия таких job и передачей параметров между ними. Когда же дело доходило до обработки иерархических структур, таких как XML или JSON, Talend вообще пасовал, ибо описать на диаграмме загрузку иерархических данных в виде одного входящего потока и множества исходящих как основного и подчиненных просто не возможно. Здесь мне приходилось вообще брать дело в свои руки и писать код на Java, который решал задачи такого уровня.

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

Вышеописанный поток моих претензий часто в той или иной мере озвучивают многие разработчики ETL при решении задач под разные отрасли. Здесь я просто все обобщил, добавив то, с чем столкнулся сам. Множество из описанного мной было успешно решено на Talend с помощью языка высокого уровня Groovy. Он полностью совместим с Java, но расширяет ее язык функционально, значительно уменьшая объем кодирования и увеличивая его читабельность. Так как Groovy все больше и чаще входит в код моих job, я решил на основе наработок с ним выделить все в отдельный framework на базе Groovy, назвав его GETL (Groovy Extract Transform Load framework). GETL будет возможным использовать как отдельную платформу для разработки job загрузки данных, так и как компонент, работающих в job Talend. Ну, и никто не мешает разработчикам Java интегрировать GETL в свои проекты как модуль и получить в свое распоряжение управляемую функциональность интеграции данных.

Чтобы быть интересным и перспективным для использования, помимо стандартного набора функционала ETL продукта GETL должен:
  • Уметь работать с динамическими и неизвестными на момент разработки структурами, включая исследование метаданных используемых объектов
  • Поддерживать разработку шаблонов загрузки и трансформации данных
  • Обеспечивать проведение автоматизированного тестирования и поддерживать unit test
  • Иметь управляемую многопоточность выполнения процессов
  • Содержать прозрачную иерархию базовых классов для легкого расширения функциональности с помощью собственных классов
  • Предоставить менеджер управления хранилищем временных файлов для проведения промежуточных операций над данными и менеджер хранения повторно используемых данных job на базе РСУБД



Компонентная схема GETL:


GETL не будет визуальным ETL, с поддержкой диаграмм, как это сейчас практикуют все современные продукты интеграции данных. Ему это просто не будет нужно - job будут описываться на базе интуитивно понятного языка Groovy, скрипты будут маленькими и внутри них будет возможно комментировать описанные алгоритмы прямо по месту. Основным плюсом визуальных ETL является упрощение описания правил маппинга, очистки и трансформации данных. В GETL будет возможность описать такие правила в коде job или через визуальный редактор, который сохранит их в области хранения повторно используемых данных, видимых далее в job. Механизм описания и выполнения таких правил будет инновационным и позволит оперировать как известными, так и не известными заранее структурами. Станет возможным описать правила маппинга без привязки к конкретным именам объектов, а так же определить набор правил очистки с определением условий, какое правило применять. Таким образом, GETL позволит описывать job как типовые шаблоны, которые смогут по однажды определенному алгоритму загружать множество разных источников данных. Нужно загрузить 100 справочников как SDT по единой схеме загрузки? Нет проблем – не нужно делать 100 job, достаточно сделать один и описать правила загрузки с учетом отличий справочников. Требуется разработать инкрементный расчет витрин хранилища данных по заданным SQL скриптам? Опять нет проблем - один job с прописанными алгоритмами, который будет можно вызвать для расчета каждой витрины.

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

Проект будет вестись по адресу: https://sourceforge.net/projects/getl
Идеи, пожелания и помощь в любом виде приветствуются.

До новых записей в блоге.