Простой framework UI ERP c помощью Vaadin

в 16:04, , рубрики: api, ERP, framework, java, UI, vaadin, первый опыт, Проектирование и рефакторинг

Хабркат

Введение

Зачем это затевалось? Год назад начали писать систему обеспечения технологической подготовки производства. И с того момента начался наш тернистый путь. Определили стек технологий с которым будем работать. Кратко описали задачу и приступили к работе.

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

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

Стек

Если кратко об используемом стеке, то использовали финский web-framework Vaadin 7.7. Это инструмент который дает возможность писать single page application практически на одном языке (Java). Т.е. с помощью языковых конструкций описывать элементы интерфейса, которые потом транслируются в HTML+JS код и показываются в браузере.

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

API

Цели преследуемые фреймворком следующие: быстрое добавление в общий интерфейс элементов
отображающих нужную структуру данных в уже привычном для всех интерфейсе. А также обеспечение работы через активную запись. Т.е. работа с выделенной строкой в таблице и добавление к ней необходимых связей.

Структура получилась следующая:

Пакет Название
Data DataContainer
TreeDataContainer
Elements BottomTabs
CommonLogic
CommonView
FilterPanel
Logic
Menu
MenuNavigator
Mode
Workspace
Permission ModifierAccess
PermissionAccess
PermissionAccessUI

Data

В пакете Data компоненты, которые необходимы для связывания (binding) данных с элементами UI. В текущей версии реализованы контейнеры, которые имеют дополнительные методы для быстрого присвоения данных в таблицы и деревья. В пакете два класса: DataContainer — абстрактный класс, на основании которого создаются производные контейнеры для хранения определенных классов данных. TreeDataContainer — реализует класса DataContainer для хранения элементов с указанием иерархии, и для отображения древовидных структур.

Примеры использования всех классов будут в следующих разделах.

Elements

Пакет, в котором находятся все классы описывающие элементы графики и логики системы.

Подход принятый к построению интерфейса — использование отдельных видов, в которых храниться все необходимые компоненты текущего UI. Использовались стандартные компоненты Vaddin — компонент View и его реализация CommonView, а также компонент навигации между видами Menu. Логика работы этих компонентов в связке взята из примера Vaadin, пример и как его сгенерить у себя с помощью maven archetype.

Реализация CommonView должна содержать в себе ссылку на реализацию интерфейса Logic или расширять уже имеющуюся реализацию CommonLogic.

Также есть перечисление Mode которое содержи в себе перечень режимов работы с имеющимися интерфейсами.

Основной графический элемент — Workspace. Это класс в котором имеется две таблицы (в
которые присваиваются данных DataContainer), основная (метод getTable()) содержит текущую информацию, таблица со списком всех элементов (метод getTableAll()) которые можно выбирать для добавления в текущий контейнер.

Навигация в Workspace реализует элемент MenuNavigator. Он описывает перечень стандартных методов работы c Workspace, такие как включение режимов Добавления и Удаления, Печати, включения панели фильтрации для таблиц, описанной в классе FilterPanel.

Для возможности редактирования добавленной информации в контейнер (установленный в таблицу из метода getTable()) используется класс BottomTabs, в который добавляются вкладки, которые в себе содержат интерфейс для редактирования информации: таблицы, поля, выпадающие списки и все что нужно.

Permission

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

ModifierAccess — перечисление имеющихся уровней доступа к UI: отключен, чтение, редактирование.

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

PermissionAccessUI — интерфейс который имплементируеся в графические компоненты, на которые назначаются права.

Реализация

Класс DataContainer — класс для хранения структур данных в виде контейнера, расширяющий BeanItemContainer.

abstract public class DataContainer<T> extends BeanItemContainer<T> {

    private ArrayList<String> captions = new ArrayList<>();
    private ArrayList<Boolean> visible = new ArrayList<>();
    private final ArrayList<String> headers = new ArrayList<>();

    public DataContainer(Class<T> type) {
        super(type);
        if (validCaption())
            initHeaders();
    }

    private boolean validCaption() {
        return captions.size() == visible.size() &&
                captions.size() == headers.size();
    }

    abstract protected void initHeaders();

    abstract public DataContainer loadAllData();

    //....
}

Создан для удобного присвоения контейнера в таблицы и деревья, за счет списков captions,
headers, visible в которых описываются какие property класса будут отображаться в виде столбцов, какие у них будут заголовки и какие из них будут свернуты.

Механизм присвоения контейнера в таблицу реализован в CommonLogic:

abstract public class CommonLogic implements Logic {

    private View view;

    public CommonLogic(View view){
        this.view = view;
    }

    public View getView(){
        return this.view;
    }

    public void setDataToTable(DataContainer container, CustomTable table) {
        if (container == null || table == null) return;

        table.setContainerDataSource(container);
        table.setVisibleColumns(container.getCaption());
        table.setColumnHeaders(container.getHeaders());
        table.setColumnCollapsingAllowed(true);
        for (int i = 0; i < container.getCaption().length; i++) {
            table.setColumnCollapsed(container.getCaption()[i],
                    container.getVisible()[i].booleanValue());
        }
    }
}

Workspace реализует в себе следующий код:


abstract public class Workspace extends CssLayout implements PermissionAccessUI {
    private Logic logic;

    private Float splitPosition = 50f;

    private Mode mode = Mode.NORMAL;

    public String CAPTION = "";
    public ThemeResource PICTURE = null;

    private FilterTable table = null;
    private FilterTable tableAll = null;

    private ItemClickEvent.ItemClickListener editItemClickListener;
    private ItemClickEvent.ItemClickListener editItemClickListenerAll;

    private VerticalSplitPanel verticalSplitPanel = null;
    private HorizontalSplitPanel horizontalSplitPanel = null;

    private BottomTabs bottomTabs = null;

    private MenuNavigator navigator = null;

    private FilterPanel filterPanel = null;

    private ModifierAccess permissionAccess = ModifierAccess.HIDE;

    private VerticalLayout layout;
    private ItemClickEvent.ItemClickListener selectItemClickListener;
    private ItemClickEvent.ItemClickListener selectItemClickListenerAll;

    public Workspace(Logic logic) {
        this.logic = logic;
        table();
        tableAll();
        navigatorLayout();
        filterPanel();
        horizontalSplitPanel();
        verticalSplitPanel();
        addComponent(verticalSplitPanel);

        editOff();
        setSizeFull();
    }
    //...
}

Где table() и tableAll() методы построения таблицы для текущего контейнера и для контейнера со всеми записями (справочника). navigatorLayout() создает меню для навигации (оно же MenuNavigator) и работы с текущим экземпляром Workspace. filterPanel() — создает панель фильтрации для таблицы с текущим контейнером. В veritcalSplitPanel() описывается создание нижней панели с закладками tabs для редактирования выбранных элементов в таблице созданной в table().

Класс MenuNavigator дает стандартный набор методов для работы с имплементацией Workspace:


public abstract class MenuNavigator extends MenuBar implements PermissionAccessUI {

    private ModifierAccess permissionAccess = ModifierAccess.HIDE;

    private MenuItem add;
    private MenuItem delete;
    private MenuItem print;
    private MenuItem filter;

    public static final String ENABLE_BUTTON_STYLE ="highlight";

    private Workspace parent;

    public MenuNavigator(String caption, Workspace parent) {
        this.parent = parent;
        setWidth("100%");
        Command addCommand = menuItem -> add();

        Command deleteCommand = menuItem -> delete();

        Command printCommand = menuItem -> print();

        Command filterCommand = menuItem -> filter();

        add = this.addItem("add" + caption, 
                                     new ThemeResource("ico16/add.png"), 
                                     addCommand);
        add.setDescription("Добавить");

        delete = this.addItem("delete" + caption, 
                                     new ThemeResource("ico16/delete.png"), 
                                     deleteCommand);
        delete.setDescription("Удалить");

        print = this.addItem("print" + caption, 
                                     new ThemeResource("ico16/printer.png"), 
                                     printCommand);
        print.setDescription("Печать");

        filter = this.addItem("filter" + caption, 
                                     new ThemeResource("ico16/filter.png"), 
                                     filterCommand);
        filter.setDescription("Сортировать");

        this.setStyleName("v-menubar-menuitem-caption-null-size");
        this.addStyleName("menu-navigator");
    }
    //...
}

В классе создаются общие элементы меню, описывается логика поведения в квази-модальном режиме и обязует реализующего этот класс описать нужную логику работы.

Редактирование выделенных записей в таблице созданной в table() происходит с помощью элементов добавленных в UI BottomTabs:


abstract public class BottomTabs extends TabSheet implements PermissionAccessUI {

    private ModifierAccess permissionAccess = ModifierAccess.HIDE;
    private final List<String> captions = new ArrayList<>();
    private final List<Component> components = new ArrayList<>();
    private final List<Resource> resources = new ArrayList<>();

    public BottomTabs() {
        captions.removeAll(captions);
        components.removeAll(components);
        resources.removeAll(resources);
        setSizeFull();
        init();
    }

    private void init() {
        initTabs();
        for (int i = 0; i < this.components.size(); i++) {
            if (i < resources.size() && i < captions.size()) {
                this.addTab(this.components.get(i)
                        , this.captions.get(i)
                        , this.resources.get(i));
            }
        }
    }
    //...
}

Здесь также реализованы списки для более быстрого добавления компонента в закладки: captions — описание заголовков закладок, components — какой элемент будет находиться в этой закладке и resource — какая иконка для него будет отображаться.

Для реализации прав доступа нужно имплементировать PermissionAccessUI и реализовать в нем методы которые должны показывать что активно в этом классе, а что нет, в зависимости от уровня доступа:


public interface PermissionAccessUI {

    void setPermissionAccess(ModifierAccess permission);

    void replacePermissionAccess(ModifierAccess permissionAccess);

    ModifierAccess getModifierAccess();

}

и ниже реализация этих методов в классе Workspace:

//...
    public void setPermissionAccess(ModifierAccess permission) {
        if (navigator != null) {
            navigator.replacePermissionAccess(permission);
        }
        if (bottomTabs != null) {
            bottomTabs.replacePermissionAccess(permission);
        }

        this.permissionAccess = permission;

        switch (permission) {
            case EDIT: {
                this.setVisible(true);
                this.setEnabled(true);
                break;
            }
            case READ: {
                this.setVisible(true);
                this.setEnabled(false);
                break;
            }
            case HIDE: {
                this.setVisible(false);
                this.setEnabled(false);
                break;
            }
        }
    }

    public void replacePermissionAccess(ModifierAccess permissionAccess) {
        PermissionAccess.replacePermissionAccess(this, permissionAccess);
    }

    public ModifierAccess getModifierAccess() {
        return permissionAccess;
    }
//...

Класс PermissionAccess — это final класс, выполняющий функцию утильного Utils класса самому не нравиться но другой реализации пока не придумал, он берет компонент PermissionAccessUI и в соответствии с заданной логикой повышает уровень доступа:


public final class PermissionAccess {
    //...
    public static void replacePermissionAccess(PermissionAccessUI component,
             ModifierAccess newValue) {
        switch (component.getModifierAccess()) {
            case EDIT: {
                if (newValue.equals(ModifierAccess.HIDE) 
                                   || newValue.equals(ModifierAccess.READ)) break;
                component.setPermissionAccess(newValue);
                break;
            }
            case READ: {
                if (newValue.equals(ModifierAccess.HIDE)) break;
                component.setPermissionAccess(newValue);
                break;
            }
            case HIDE: {
                component.setPermissionAccess(newValue);
                break;
            }
        }
    }
    //...
}

Примеры

Данные

Пример создания контейнера какого-то абстрактного класса описывающего предметную область (он же является Bean), назовем его Element:


public class Element implements Serializable {
    private Integer id = 0;
    private String name = "element";
    private Float price = 0.0F;

    public Element(Integer id, String name, Float price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Float getPrice() {
        return price;
    }

    public void setPrice(Float price) {
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Element element = (Element) o;
        return Objects.equals(id, element.id) &&
                Objects.equals(name, element.name) &&
                Objects.equals(price, element.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, price);
    }
}

Классическая реализация в соответствии со спецификацией для Bean.

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


public class ElementContainer extends DataContainer<Element> {
    public ElementContainer() {
        super(Element.class);
    }

    @Override
    protected void initHeaders() {
        addCaption("id", "name", "price");
        addHeader("ID", "Название", "Цена");
        addCollapsed(true, false, false);
    }

    @Override
    public DataContainer loadAllData() {
        add(new Element(1, "name1", 1.0f));
        add(new Element(2, "name2", 2.0f));
        add(new Element(3, "name3", 3.0f));
        add(new Element(4, "name4", 4.0f));
        add(new Element(5, "name5", 5.0f));
        add(new Element(6, "name6", 6.0f));
        add(new Element(7, "name7", 7.0f));
        add(new Element(8, "name8", 8.0f));
        add(new Element(9, "name9", 9.0f));
        add(new Element(10, "name10", 10.0f));
        add(new Element(11, "name11", 11.0f));
        return this;
    }
}

Где в методах addCaption, addHeader, addCollapsed перечисляются property класса Element, которые будут использоваться в виде колонок, в какой последовательности, какие заголовки и какие из них будут скрыты.

Реализация классов для UI

Реализация класса Workspace в виде класса MyLayout:

public class MyLayout extends Workspace {
    private ElementContainer container = new ElementContainer();
    private MyTabSheet tabSheet;
    private MyMenu menu;

    public MyLayout(Logic logic) {
        super(logic);
        tabSheet = new MyTabSheet();
        menu = new MyMenu("myMenu", this);
        logic.setDataToTable(container.loadAllData(), getTable()); 
        setBottomTabs(tabSheet);
        setNavigator(menu);
    }

    @Override
    protected ItemClickEvent.ItemClickListener editTableItemClick() {
        return itemClickEvent -> {
        };
    }

    @Override
    protected ItemClickEvent.ItemClickListener selectTableItemClick() {
        return itemClickEvent -> {
        };
    }

    @Override
    protected ItemClickEvent.ItemClickListener editTableAllItemClick() {
        return itemClickEvent -> {
        };
    }

    @Override
    protected ItemClickEvent.ItemClickListener selectTableAllItemClick() {
        return itemClickEvent -> {
        };
    }
}

Где описывается поведение при выборе записи в таблице со всеми компонентами и текущим контейнером (методы ItemClickEvent.ItemClickListener), здесь они пустые. Также logic.setDataToTable(container.loadAllData(), getTable()) здесь описывается приме установки текущего контейнера в таблицу.

Реализация MenuNavigator в классе MyMenu:

public class MyMenu extends MenuNavigator {

    public MyMenu(String caption, Workspace parent) {
        super(caption, parent);
    }

    @Override
    public void add() {
        if (getAdd().getStyleName() == null)
            getAdd().setStyleName(ENABLE_BUTTON_STYLE);
        else
            getAdd().setStyleName(null);
    }

    @Override
    public void delete() {
        if (getDelete().getStyleName() == null)
            getDelete().setStyleName(ENABLE_BUTTON_STYLE);
        else
            getDelete().setStyleName(null);
    }

    @Override
    public void print() {
        if (getPrint().getStyleName() == null)
            getPrint().setStyleName(ENABLE_BUTTON_STYLE);
        else
            getPrint().setStyleName(null);

    }
}

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

И последний элемент описывающий графику MyTabSheet — реализация BottomTabs:

public class MyTabSheet extends BottomTabs {
    public MyTabSheet() {
        super();
    }

    @Override
    public void initTabs() {
        addCaption("Tab1", "Tab2", "Tab3", "Tab4");

        addComponent(new Label("label1"),
                new Label("label2"),
                new Label("label3"),
                new Label("label4"));

        addResource(FontAwesome.AMAZON,
                FontAwesome.AMAZON,
                FontAwesome.AMAZON,
                FontAwesome.AMAZON
        );
    }
}

Где создаются 4 закладки, в которые устанавливаются компоненты Label, и на все закладки ставится значок Amazon, не сочтите за рекламу, просто буква А идет первой.

В итоге получается вот такой интерфейс:

Картинка с GitHub

Заключение

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

PS

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

Благодарности

Ткаченко Евгению за разработку класса для фильтрации FilterPanel и активное участие в проекте.

Ссылки

Ссылка на репозиторий, там же описание как подключить. И пока доступен только SNAPSHOT, но надеюсь на скорый релиз.

Автор: sah4ez32

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js