com.haulmont.cuba.gui.components.filter.FilterDelegateImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.gui.components.filter.FilterDelegateImpl.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.haulmont.cuba.gui.components.filter;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.haulmont.bali.datastruct.Node;
import com.haulmont.bali.util.Dom4j;
import com.haulmont.bali.util.ParamsMap;
import com.haulmont.chile.core.datatypes.Datatypes;
import com.haulmont.chile.core.model.utils.InstanceUtils;
import com.haulmont.cuba.client.ClientConfig;
import com.haulmont.cuba.client.sys.PersistenceManagerClient;
import com.haulmont.cuba.core.app.DataService;
import com.haulmont.cuba.core.app.dynamicattributes.DynamicAttributesUtils;
import com.haulmont.cuba.core.entity.AbstractSearchFolder;
import com.haulmont.cuba.core.entity.AppFolder;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.global.filter.DenyingClause;
import com.haulmont.cuba.core.global.filter.QueryFilter;
import com.haulmont.cuba.gui.ComponentsHelper;
import com.haulmont.cuba.gui.WindowManager;
import com.haulmont.cuba.gui.WindowManagerProvider;
import com.haulmont.cuba.gui.WindowParams;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.Action.Status;
import com.haulmont.cuba.gui.components.Component.Alignment;
import com.haulmont.cuba.gui.components.DialogAction.Type;
import com.haulmont.cuba.gui.components.Frame.MessageType;
import com.haulmont.cuba.gui.components.KeyCombination.Key;
import com.haulmont.cuba.gui.components.actions.BaseAction;
import com.haulmont.cuba.gui.components.actions.ItemTrackingAction;
import com.haulmont.cuba.gui.components.filter.condition.*;
import com.haulmont.cuba.gui.components.filter.edit.FilterEditor;
import com.haulmont.cuba.gui.components.filter.filterselect.FilterSelectWindow;
import com.haulmont.cuba.gui.config.WindowConfig;
import com.haulmont.cuba.gui.config.WindowInfo;
import com.haulmont.cuba.gui.data.CollectionDatasource;
import com.haulmont.cuba.gui.data.HierarchicalDatasource;
import com.haulmont.cuba.gui.presentations.Presentations;
import com.haulmont.cuba.gui.settings.SettingsImpl;
import com.haulmont.cuba.gui.theme.ThemeConstants;
import com.haulmont.cuba.gui.theme.ThemeConstantsManager;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.security.entity.FilterEntity;
import com.haulmont.cuba.security.entity.SearchFolder;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Attribute;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@org.springframework.stereotype.Component(FilterDelegate.NAME)
@Scope("prototype")
public class FilterDelegateImpl implements FilterDelegate {

    protected static final String BORDER_HIDDEN_STYLENAME = "border-hidden";

    protected static final String GLOBAL_FILTER_PERMISSION = "cuba.gui.filter.global";
    protected static final String GLOBAL_APP_FOLDERS_PERMISSION = "cuba.gui.appFolder.global";
    protected static final String FILTER_EDIT_PERMISSION = "cuba.gui.filter.edit";
    protected static final String CONDITIONS_LOCATION_TOP = "top";

    protected static final Logger log = LoggerFactory.getLogger(FilterDelegateImpl.class);
    protected static final String MODIFIED_INDICATOR_SYMBOL = " *";

    @Inject
    protected ComponentsFactory componentsFactory;
    @Inject
    protected ThemeConstantsManager themeConstantsManager;
    @Inject
    protected Messages messages;
    @Inject
    protected WindowManagerProvider windowManagerProvider;
    @Inject
    protected Metadata metadata;
    @Inject
    protected WindowConfig windowConfig;
    @Inject
    protected UserSessionSource userSessionSource;
    @Inject
    protected Configuration configuration;
    @Inject
    protected Security security;
    @Inject
    protected FilterHelper filterHelper;
    @Inject
    protected FilterParser filterParser;
    @Inject
    protected MaxResultsFieldHelper maxResultsFieldHelper;

    protected FtsFilterHelper ftsFilterHelper;
    protected DataService dataService;
    protected PersistenceManagerClient persistenceManager;
    protected ClientConfig clientConfig;
    protected GlobalConfig globalConfig;
    protected AddConditionHelper addConditionHelper;
    protected ThemeConstants theme;
    protected WindowManager windowManager;

    protected Filter filter;
    protected FilterEntity adHocFilter;
    protected ConditionsTree conditions = new ConditionsTree();
    protected ConditionsTree prevConditions = new ConditionsTree();
    protected List<AbstractCondition> initialConditions = new ArrayList<>();
    protected FilterEntity filterEntity;
    protected FilterEntity initialFilterEntity;
    protected CollectionDatasource datasource;
    protected QueryFilter dsQueryFilter;
    protected List<FilterEntity> filterEntities = new ArrayList<>();
    protected AppliedFilter lastAppliedFilter;
    protected LinkedList<AppliedFilterHolder> appliedFilters = new LinkedList<>();
    protected List<Filter.FilterEntityChangeListener> filterEntityChangeListeners = new ArrayList<>();

    protected GroupBoxLayout groupBoxLayout;
    protected BoxLayout layout;
    protected PopupButton filtersPopupButton;
    protected Component.Container conditionsLayout;
    protected BoxLayout maxResultsLayout;
    protected Field maxResultsField;
    protected TextField maxResultsTextField;
    protected LookupField maxResultsLookupField;
    protected BoxLayout controlsLayout;
    protected Component.Container appliedFiltersLayout;
    protected PopupButton settingsBtn;
    protected Component applyTo;
    protected SaveAction saveAction;
    protected SaveAction saveWithValuesAction;
    protected TextField ftsSearchCriteriaField;
    protected CheckBox ftsSwitch;
    protected LinkButton addConditionBtn;
    protected Component.Container filtersPopupBox;
    protected Button searchBtn;
    protected Component controlsLayoutGap;
    protected Object paramEditComponentToFocus;

    protected String caption;
    protected int maxResults = -1;
    protected boolean useMaxResults;
    protected boolean textMaxResults;
    protected Boolean manualApplyRequired;
    protected boolean folderActionsEnabled = true;
    protected boolean filtersLookupListenerEnabled = true;
    protected boolean filtersPopupDisplayed = false;
    protected boolean filtersLookupDisplayed = false;
    protected boolean maxResultsAddedToLayout = false;
    protected boolean editable = true;
    protected FilterMode filterMode;
    protected boolean filterSavingPossible = true;
    protected Integer columnsCount;
    protected String initialWindowCaption;
    protected String conditionsLocation;
    protected boolean filterActionsCreated = false;
    protected boolean delayedFocus;
    protected boolean modeSwitchVisible = true;

    protected SaveAsAction saveAsAction;
    protected EditAction editAction;
    protected MakeDefaultAction makeDefaultAction;
    protected RemoveAction removeAction;
    protected PinAppliedAction pinAppliedAction;
    protected SaveAsFolderAction saveAsAppFolderAction;
    protected SaveAsFolderAction saveAsSearchFolderAction;
    protected LookupField filtersLookup;

    protected List<FDExpandedStateChangeListener> expandedStateChangeListeners;

    protected Filter.BeforeFilterAppliedHandler beforeFilterAppliedHandler;

    protected Filter.AfterFilterAppliedHandler afterFilterAppliedHandler;
    protected boolean borderVisible = true;

    protected Set<String> ftsLastDatasourceRefreshParamsNames = new HashSet<>();

    protected enum ConditionsFocusType {
        NONE, FIRST, LAST
    }

    @PostConstruct
    public void init() {
        theme = themeConstantsManager.getConstants();
        windowManager = windowManagerProvider.get();
        dataService = AppBeans.get(DataService.class);
        persistenceManager = AppBeans.get(PersistenceManagerClient.class);
        globalConfig = configuration.getConfig(GlobalConfig.class);
        clientConfig = configuration.getConfig(ClientConfig.class);
        if (AppBeans.containsBean(FtsFilterHelper.NAME)) {
            ftsFilterHelper = AppBeans.get(FtsFilterHelper.class);
        }
        filterMode = FilterMode.GENERIC_MODE;

        conditionsLocation = clientConfig.getGenericFilterConditionsLocation();
        createLayout();
    }

    protected void createLayout() {
        if (layout == null) {
            groupBoxLayout = componentsFactory.createComponent(GroupBoxLayout.class);
            groupBoxLayout.addExpandedStateChangeListener(e -> fireExpandStateChange());
            groupBoxLayout.setOrientation(GroupBoxLayout.Orientation.VERTICAL);
            groupBoxLayout.setWidth("100%");
            layout = componentsFactory.createComponent(VBoxLayout.class);
            layout.setWidth("100%");
            groupBoxLayout.add(layout);
            if (caption == null)
                setCaption(getMainMessage("filter.groupBoxCaption"));
        } else {
            Collection<Component> components = layout.getComponents();
            for (Component component : components) {
                layout.remove(component);
            }
        }
        layout.setSpacing(true);

        appliedFiltersLayout = componentsFactory.createComponent(VBoxLayout.class);

        conditionsLayout = componentsFactory.createComponent(HBoxLayout.class);
        conditionsLayout.setVisible(false); // initially hidden
        conditionsLayout.setWidth("100%");
        conditionsLayout.setStyleName("filter-conditions");

        if (filterMode == FilterMode.GENERIC_MODE) {
            createControlsLayoutForGeneric();
        } else {
            createControlsLayoutForFts();
        }

        if (CONDITIONS_LOCATION_TOP.equals(conditionsLocation)) {
            layout.add(conditionsLayout);
            layout.add(controlsLayout);
        } else {
            layout.add(controlsLayout);
            layout.add(conditionsLayout);
        }
    }

    protected void createControlsLayoutForGeneric() {
        controlsLayout = componentsFactory.createComponent(HBoxLayout.class);
        controlsLayout.setSpacing(true);
        controlsLayout.setWidth("100%");
        filterHelper.setInternalDebugId(controlsLayout, "controlsLayout");

        filtersPopupBox = filterHelper.createSearchButtonGroupContainer();
        filtersPopupBox.addStyleName("filter-search-button-layout");
        filterHelper.setInternalDebugId(filtersPopupBox, "filtersPopupBox");

        searchBtn = componentsFactory.createComponent(Button.class);
        filtersPopupBox.add(searchBtn);
        searchBtn.setStyleName("filter-search-button");
        searchBtn.setCaption(getMainMessage("filter.search"));
        searchBtn.setIcon("icons/search.png");
        searchBtn.setAction(new AbstractAction("search") {
            @Override
            public void actionPerform(Component component) {
                apply(false);
            }
        });
        filterHelper.setInternalDebugId(searchBtn, "searchBtn");

        filtersPopupButton = componentsFactory.createComponent(PopupButton.class);
        filtersPopupButton.setStyleName("icon-only");
        filterHelper.setInternalDebugId(filtersPopupButton, "filtersPopupButton");
        filtersPopupBox.add(filtersPopupButton);

        filtersLookup = componentsFactory.createComponent(LookupField.class);
        filtersLookup.setWidth(theme.get("cuba.gui.filter.select.width"));
        filtersLookup.addValueChangeListener(new FiltersLookupChangeListener());
        filterHelper.setLookupNullSelectionAllowed(filtersLookup, false);
        filterHelper.setInternalDebugId(filtersLookup, "filtersLookup");

        addConditionBtn = componentsFactory.createComponent(LinkButton.class);
        addConditionBtn.setAlignment(Alignment.MIDDLE_LEFT);
        addConditionBtn.setCaption(getMainMessage("filter.addCondition"));
        addConditionBtn.setAction(new AbstractAction("openAddConditionDlg") {
            @Override
            public void actionPerform(Component component) {
                addConditionHelper.addCondition(conditions);
            }
        });
        filterHelper.setInternalDebugId(addConditionBtn, "addConditionBtn");

        controlsLayoutGap = componentsFactory.createComponent(Label.class);
        filterHelper.setInternalDebugId(controlsLayoutGap, "controlsLayoutGap");
        controlsLayout.add(controlsLayoutGap);
        controlsLayout.expand(controlsLayoutGap);

        settingsBtn = componentsFactory.createComponent(PopupButton.class);
        settingsBtn.setIcon("icons/gear.png");
        settingsBtn.setStyleName("filter-settings-button");
        filterHelper.setInternalDebugId(settingsBtn, "settingsBtn");
        createFilterActions();

        createMaxResultsLayout();
        createFtsSwitch();
        ftsSwitch.setAlignment(Alignment.MIDDLE_RIGHT);
        filterHelper.setInternalDebugId(ftsSwitch, "ftsSwitch");

        String layoutDescription = clientConfig.getGenericFilterControlsLayout();
        ControlsLayoutBuilder controlsLayoutBuilder = createControlsLayoutBuilder(layoutDescription);
        controlsLayoutBuilder.build();
        if (isMaxResultsLayoutVisible()) {
            initMaxResults();
        }

        maxResultsLayout.setVisible(isMaxResultsLayoutVisible());
        filterHelper.setInternalDebugId(maxResultsLayout, "maxResultsLayout");
    }

    protected void createControlsLayoutForFts() {
        controlsLayout = componentsFactory.createComponent(HBoxLayout.class);
        controlsLayout.setSpacing(true);
        controlsLayout.setWidthFull();

        ftsSearchCriteriaField = componentsFactory.createComponent(TextField.class);
        ftsSearchCriteriaField.setWidth(theme.get("cuba.gui.filter.ftsSearchCriteriaField.width"));
        ftsSearchCriteriaField.setInputPrompt(getMainMessage("filter.enterSearchPhrase"));
        filterHelper.setInternalDebugId(ftsSearchCriteriaField, "ftsSearchCriteriaField");
        filterHelper.addShortcutListener(ftsSearchCriteriaField, createFtsSearchShortcutListener());

        paramEditComponentToFocus = ftsSearchCriteriaField;
        controlsLayout.add(ftsSearchCriteriaField);

        searchBtn = componentsFactory.createComponent(Button.class);
        searchBtn.setCaption(getMainMessage("filter.search"));
        searchBtn.setIcon("icons/search.png");
        searchBtn.setAction(new AbstractAction("search") {
            @Override
            public void actionPerform(Component component) {
                applyFts();
            }
        });
        controlsLayout.add(searchBtn);

        controlsLayoutGap = componentsFactory.createComponent(Label.class);
        controlsLayout.add(controlsLayoutGap);
        controlsLayout.expand(controlsLayoutGap);

        createMaxResultsLayout();
        if (isMaxResultsLayoutVisible()) {
            controlsLayout.add(maxResultsLayout);
            initMaxResults();
        }

        createFtsSwitch();
        ftsSwitch.setAlignment(Alignment.MIDDLE_RIGHT);
        controlsLayout.add(ftsSwitch);
    }

    protected FilterHelper.ShortcutListener createFtsSearchShortcutListener() {
        return new FilterHelper.ShortcutListener("ftsSearch", new KeyCombination(Key.ENTER)) {
            @Override
            public void handleShortcutPressed() {
                applyFts();
            }
        };
    }

    protected void createFtsSwitch() {
        ftsSwitch = componentsFactory.createComponent(CheckBox.class);
        ftsSwitch.setCaption(getMainMessage("filter.ftsSwitch"));
        ftsSwitch.setValue(filterMode == FilterMode.FTS_MODE);

        ftsSwitch.addValueChangeListener(e -> {
            filterMode = Boolean.TRUE.equals(e.getValue()) ? FilterMode.FTS_MODE : FilterMode.GENERIC_MODE;
            switchFilterMode(filterMode);
        });

        ftsSwitch.setVisible(modeSwitchVisible);
    }

    @Override
    public void switchFilterMode(FilterMode filterMode) {
        if (filterMode == FilterMode.FTS_MODE && !isFtsModeEnabled()) {
            log.warn("Unable to switch to the FTS filter mode. FTS mode is not supported for the {} entity",
                    datasource.getMetaClass().getName());
            return;
        }
        this.filterMode = filterMode;
        if (filterMode == FilterMode.FTS_MODE) {
            prevConditions = conditions;
            ((CollectionDatasource.SupportsApplyToSelected) datasource).unpinAllQuery();
            appliedFilters.clear();
            lastAppliedFilter = null;
        }
        conditions = (filterMode == FilterMode.GENERIC_MODE) ? prevConditions : new ConditionsTree();
        createLayout();

        if (filterMode == FilterMode.GENERIC_MODE) {
            fillConditionsLayout(ConditionsFocusType.FIRST);
            addConditionBtn.setVisible(editable && userCanEditFilers());
            setFilterActionsEnabled();
            initFilterSelectComponents();
        }
        if (paramEditComponentToFocus != null)
            requestFocusToParamEditComponent();
        else if (filtersPopupDisplayed)
            filtersPopupButton.requestFocus();
        else if (filtersLookupDisplayed) {
            filtersLookup.requestFocus();
        }
        updateWindowCaption();
    }

    protected void createMaxResultsLayout() {
        maxResultsLayout = componentsFactory.createComponent(HBoxLayout.class);
        maxResultsLayout.setStyleName("c-maxresults");
        maxResultsLayout.setSpacing(true);
        Label maxResultsLabel = componentsFactory.createComponent(Label.class);
        maxResultsLabel.setStyleName("c-maxresults-label");
        maxResultsLabel.setValue(messages.getMainMessage("filter.maxResults.label1"));
        maxResultsLabel.setAlignment(Alignment.MIDDLE_RIGHT);
        maxResultsLayout.add(maxResultsLabel);

        maxResultsTextField = componentsFactory.createComponent(TextField.class);
        maxResultsTextField.setStyleName("c-maxresults-input");
        maxResultsTextField.setMaxLength(4);
        maxResultsTextField.setWidth(theme.get("cuba.gui.Filter.maxResults.width"));
        maxResultsTextField.setDatatype(Datatypes.get("int"));

        maxResultsLookupField = maxResultsFieldHelper.createMaxResultsLookupField();
        maxResultsLookupField.setStyleName("c-maxresults-select");

        maxResultsField = textMaxResults ? maxResultsTextField : maxResultsLookupField;
        maxResultsLayout.add(maxResultsField);
    }

    @Override
    public void setBorderVisible(boolean visible) {
        borderVisible = visible;

        if (visible) {
            groupBoxLayout.removeStyleName(BORDER_HIDDEN_STYLENAME);
        } else {
            groupBoxLayout.addStyleName(BORDER_HIDDEN_STYLENAME);
        }
    }

    @Override
    public boolean isBorderVisible() {
        return borderVisible;
    }

    /**
     * Loads filter entities, finds default filter and applies it if found
     */
    @Override
    public void loadFiltersAndApplyDefault() {
        initShortcutActions();
        initAdHocFilter();
        loadFilterEntities();
        FilterEntity defaultFilter = getDefaultFilter(filterEntities);
        initFilterSelectComponents();

        if (defaultFilter == null) {
            defaultFilter = adHocFilter;
        }

        try {
            setFilterEntity(defaultFilter);
        } catch (Exception e) {
            log.error("Exception on loading default filter '" + defaultFilter.getName() + "'", e);
            windowManager.showNotification(
                    messages.formatMainMessage("filter.errorLoadingDefaultFilter", defaultFilter.getName()),
                    Frame.NotificationType.ERROR);
            defaultFilter = adHocFilter;
            setFilterEntity(adHocFilter);
        }

        if (defaultFilter != adHocFilter && (filterMode == FilterMode.GENERIC_MODE)) {
            Window window = ComponentsHelper.getWindow(filter);
            if (!WindowParams.DISABLE_AUTO_REFRESH.getBool(window.getContext())) {
                if (getResultingManualApplyRequired()) {
                    if (BooleanUtils.isTrue(defaultFilter.getApplyDefault())) {
                        apply(true);
                    }
                } else
                    apply(true);
                if (filterEntity != null) {
                    window.setDescription(getFilterCaption(filterEntity));
                } else
                    window.setDescription(null);
            }
        }
    }

    protected boolean suitableCondition(AbstractCondition condition) {
        if (condition instanceof PropertyCondition) {
            return datasource.getMetaClass().getPropertyPath(condition.getName()) != null;
        }

        if (condition instanceof DynamicAttributesCondition) {
            return DynamicAttributesUtils.getMetaPropertyPath(datasource.getMetaClass(),
                    ((DynamicAttributesCondition) condition).getCategoryAttributeId()) != null;
        }

        return true;
    }

    /**
     * Sets filter entity, creates condition editor components and applies filter if necessary
     */
    @Override
    public void setFilterEntity(FilterEntity filterEntity) {
        this.filterEntity = filterEntity;
        conditions = filterParser.getConditions(filter, filterEntity.getXml());
        prevConditions = conditions;
        initialConditions = conditions.toConditionsList();

        for (AbstractCondition condition : initialConditions) {
            if (!suitableCondition(condition)) {
                String message = String.format(getMainMessage("filter.inappropriate.filter"),
                        filterEntity.getName(), datasource.getMetaClass().getName());

                windowManager.showNotification(message, Frame.NotificationType.HUMANIZED);
                setFilterEntity(adHocFilter);
                break;
            }

            condition.addListener(new AbstractCondition.Listener() {
                @Override
                public void captionChanged() {
                }

                @Override
                public void paramChanged(Param oldParam, Param newParam) {
                    updateFilterModifiedIndicator();
                }
            });
        }

        // If there are window parameters named as filter parameters, assign values to the corresponding
        // filter params. Together with passing a filter code in 'filter' window parameter it allows to open an
        // arbitrary filter with parameters regardless of a user defined default filter.
        Window window = ComponentsHelper.getWindow(filter);
        for (AbstractCondition condition : conditions.toConditionsList()) {
            if (condition.getParam() != null) {
                for (Map.Entry<String, Object> entry : window.getContext().getParams().entrySet()) {
                    if (entry.getKey().equals(condition.getParam().getName()))
                        condition.getParam().parseValue((String) entry.getValue());
                }
            }
        }

        saveInitialFilterState();

        if (filtersLookupDisplayed) {
            filtersLookupListenerEnabled = false;
            filtersLookup.setValue(filterEntity);
            filtersLookupListenerEnabled = true;
        }

        setFilterActionsEnabled();
        setFilterActionsVisible();
        fillConditionsLayout(ConditionsFocusType.FIRST);
        if (delayedFocus) {
            delayedFocus = false;
            requestFocus();
        } else {
            requestFocusToParamEditComponent();
        }

        setConditionsLayoutVisible(true);

        if (BooleanUtils.isTrue(filterEntity.getIsSet())
                || (filterEntity.getFolder() != null && BooleanUtils.isNotFalse(filterEntity.getApplyDefault()))) {
            apply(true);
        }

        for (Filter.FilterEntityChangeListener listener : filterEntityChangeListeners) {
            listener.filterEntityChanged(filterEntity);
        }

        updateWindowCaption();
    }

    /**
     * Saves initial filter state. It is used for indicating of filter modifications
     */
    protected void saveInitialFilterState() {
        initialFilterEntity = metadata.create(FilterEntity.class);
        initialFilterEntity.setName(filterEntity.getName());
        initialFilterEntity.setCode(filterEntity.getCode());
        initialFilterEntity.setUser(filterEntity.getUser());
        initialFilterEntity.setXml(filterEntity.getXml());
    }

    /**
     * Sets conditionsLayout visibility and shows/hides top border of controlsLayout
     */
    protected void setConditionsLayoutVisible(boolean visible) {
        conditionsLayout.setVisible(visible && !conditionsLayout.getComponents().isEmpty());

        controlsLayout.setStyleName(getControlsLayoutStyleName());
    }

    protected void setFilterActionsEnabled() {
        boolean isGlobal = filterEntity.getUser() == null;
        boolean userCanEditGlobalFilter = uerCanEditGlobalFilter();
        boolean userCanEditFilters = userCanEditFilers();
        boolean filterEditable = isEditable();
        boolean userCanEditGlobalAppFolder = userSessionSource.getUserSession()
                .isSpecificPermitted(GLOBAL_APP_FOLDERS_PERMISSION);
        boolean createdByCurrentUser = userSessionSource.getUserSession().getCurrentOrSubstitutedUser()
                .equals(filterEntity.getUser());
        boolean hasCode = !Strings.isNullOrEmpty(filterEntity.getCode());
        boolean isFolder = filterEntity.getFolder() != null;
        boolean isSearchFolder = isFolder && (filterEntity.getFolder() instanceof SearchFolder);
        boolean isAppFolder = isFolder && (filterEntity.getFolder() instanceof AppFolder);
        boolean isSet = BooleanUtils.isTrue(filterEntity.getIsSet());
        boolean isDefault = BooleanUtils.isTrue(filterEntity.getIsDefault());
        boolean isAdHocFilter = filterEntity == adHocFilter;

        boolean editActionEnabled = !isSet && filterEditable && userCanEditFilters
                && (!isGlobal || userCanEditGlobalFilter);
        filterSavingPossible = editActionEnabled
                && ((isGlobal && userCanEditGlobalFilter) || (!isGlobal && createdByCurrentUser))
                && ((!isFolder && !hasCode) || isSearchFolder || (isAppFolder && userCanEditGlobalAppFolder));
        boolean saveActionEnabled = filterSavingPossible && (isFolder || isFilterModified());
        boolean saveAsActionEnabled = !isSet && filterEditable && userCanEditFilters;
        boolean removeActionEnabled = !isSet && (!hasCode && !isFolder)
                && ((isGlobal && userCanEditGlobalFilter) || (!isGlobal && createdByCurrentUser)) && !isAdHocFilter
                && filterEditable && userCanEditFilters;
        boolean makeDefaultActionEnabled = !isDefault && !isFolder && !isSet && !isAdHocFilter
                && (!isGlobal || userCanEditGlobalFilter);
        boolean pinAppliedActionEnabled = lastAppliedFilter != null
                && !(lastAppliedFilter.getFilterEntity() == adHocFilter
                        && lastAppliedFilter.getConditions().getRoots().size() == 0)
                && (datasource == null
                        || Stores.isMain(metadata.getTools().getStoreName(datasource.getMetaClass())));
        boolean saveAsSearchFolderActionEnabled = folderActionsEnabled && !isFolder && !hasCode;
        boolean saveAsAppFolderActionEnabled = folderActionsEnabled && !isFolder && !hasCode
                && userCanEditGlobalAppFolder;

        saveAction.setEnabled(saveActionEnabled);
        saveWithValuesAction.setEnabled(filterSavingPossible && !conditions.toConditionsList().isEmpty());
        saveAsAction.setEnabled(saveAsActionEnabled);
        editAction.setEnabled(editActionEnabled);
        removeAction.setEnabled(removeActionEnabled);
        makeDefaultAction.setEnabled(makeDefaultActionEnabled);
        pinAppliedAction.setEnabled(pinAppliedActionEnabled);
        saveAsSearchFolderAction.setEnabled(saveAsSearchFolderActionEnabled);
        saveAsAppFolderAction.setEnabled(saveAsAppFolderActionEnabled);

        if (filterHelper.isTableActionsEnabled()) {
            fillTableActions();
        }
    }

    protected void setFilterActionsVisible() {
        saveAsSearchFolderAction.setVisible(folderActionsEnabled);
        saveAsAppFolderAction.setVisible(folderActionsEnabled);
        saveAction.setVisible(editable);
        saveWithValuesAction.setVisible(editable);
        saveAsAction.setVisible(editable);
        editAction.setVisible(editable);
        removeAction.setVisible(editable);
    }

    protected void createFilterActions() {
        saveAction = new SaveAction("save", false, getMainMessage("filter.save"));
        saveWithValuesAction = new SaveAction("save_with_values", true, getMainMessage("filter.saveWithValues"));
        saveAsAction = new SaveAsAction();
        editAction = new EditAction();
        makeDefaultAction = new MakeDefaultAction();
        removeAction = new RemoveAction();
        pinAppliedAction = new PinAppliedAction();
        saveAsAppFolderAction = new SaveAsFolderAction(true);
        saveAsSearchFolderAction = new SaveAsFolderAction(false);
        filterActionsCreated = true;
    }

    protected boolean uerCanEditGlobalFilter() {
        return userSessionSource.getUserSession().isSpecificPermitted(GLOBAL_FILTER_PERMISSION);
    }

    protected boolean userCanEditFilers() {
        return security.isSpecificPermitted(FILTER_EDIT_PERMISSION);
    }

    protected void saveFilterEntity() {
        Boolean isDefault = filterEntity.getIsDefault();
        Boolean applyDefault = filterEntity.getApplyDefault();
        if (filterEntity.getFolder() == null) {
            CommitContext ctx = new CommitContext(Collections.singletonList(filterEntity));
            Set<Entity> result = dataService.commit(ctx);
            FilterEntity savedFilterEntity = (FilterEntity) result.iterator().next();
            filterEntities.remove(filterEntity);
            filterEntity = savedFilterEntity;
            filterEntities.add(filterEntity);

            filterEntity.setApplyDefault(applyDefault);
            filterEntity.setIsDefault(isDefault);
        } else {
            filterEntity.getFolder().setName(filterEntity.getName());
            filterEntity.getFolder().setFilterXml(filterEntity.getXml());
            AbstractSearchFolder folder = saveFolder(filterEntity.getFolder());
            filterEntity.setFolder(folder);
        }

        saveInitialFilterState();
        setFilterActionsEnabled();
        updateFilterModifiedIndicator();
    }

    @Nullable
    protected AbstractSearchFolder saveFolder(AbstractSearchFolder folder) {
        return filterHelper.saveFolder(folder);
    }

    protected void saveAsFolder(boolean isAppFolder) {
        final AbstractSearchFolder folder;
        if (isAppFolder)
            folder = (metadata.create(AppFolder.class));
        else
            folder = (metadata.create(SearchFolder.class));

        if (filterEntity.getCode() == null) {
            String folderName = filterEntity != adHocFilter ? filterEntity.getName() : "";
            folder.setName(folderName);
            folder.setTabName(folderName);
        } else {
            String name = messages.getMainMessage(filterEntity.getCode());
            folder.setName(name);
            folder.setTabName(name);
        }

        String newXml = filterParser.getXml(conditions, Param.ValueProperty.VALUE);

        folder.setFilterComponentId(filterEntity.getComponentId());
        folder.setFilterXml(newXml);
        if (!isAppFolder) {
            if (uerCanEditGlobalFilter())
                ((SearchFolder) folder).setUser(filterEntity.getUser());
            else
                ((SearchFolder) folder).setUser(userSessionSource.getUserSession().getCurrentOrSubstitutedUser());
        }
        Presentations presentations;
        if (applyTo != null && applyTo instanceof Component.HasPresentations) {
            final Component.HasPresentations presentationsOwner = (Component.HasPresentations) applyTo;
            presentations = presentationsOwner.isUsePresentations() ? presentationsOwner.getPresentations() : null;
        } else {
            presentations = null;
        }

        Runnable commitHandler;
        if (isAppFolder) {
            commitHandler = () -> {
                AbstractSearchFolder savedFolder = saveFolder(folder);
                filterEntity.setFolder(savedFolder);
            };
        } else {
            commitHandler = () -> {
                AbstractSearchFolder savedFolder = saveFolder(folder);
                filterEntity.setFolder(savedFolder);
            };
        }

        filterHelper.openFolderEditWindow(isAppFolder, folder, presentations, commitHandler);

    }

    /**
     * Removes all components from conditionsLayout and fills it with components for editing filter conditions
     *
     * @param conditionsFocusType where to set focus (first condition, last condition, no focus)
     */
    protected void fillConditionsLayout(ConditionsFocusType conditionsFocusType) {
        for (Component component : conditionsLayout.getComponents()) {
            conditionsLayout.remove(component);
        }

        paramEditComponentToFocus = null;

        recursivelyCreateConditionsLayout(conditionsFocusType, false, conditions.getRootNodes(), conditionsLayout,
                0);

        conditionsLayout.setVisible(!conditionsLayout.getComponents().isEmpty());
    }

    protected void recursivelyCreateConditionsLayout(ConditionsFocusType conditionsFocusType,
            boolean initialFocusSet, List<Node<AbstractCondition>> nodes, Component.Container parentContainer,
            int level) {
        List<Node<AbstractCondition>> visibleConditionNodes = fetchVisibleNodes(nodes);

        if (visibleConditionNodes.isEmpty()) {
            if (level == 0)
                controlsLayout.setStyleName("filter-control-no-border");
            return;
        }

        //note that this is not grid columns count, but number of conditions (label cell + value cell) in one row
        int conditionsCount = getColumnsCount();
        int row = 0;
        int nextColumnStart = 0;
        GridLayout grid = componentsFactory.createComponent(GridLayout.class);
        grid.setStyleName("conditions-grid");
        grid.setColumns(conditionsCount * 2);
        //set expand ratio only for cells with param edit components
        for (int i = 0; i < conditionsCount; i++) {
            grid.setColumnExpandRatio(i * 2 + 1, 1);
        }
        grid.setRows(1);
        grid.setSpacing(true);
        grid.setWidth("100%");

        ParamEditor firstParamEditor = null;
        ParamEditor lastParamEditor = null;

        boolean currentFocusSet = initialFocusSet;

        for (int i = 0; i < visibleConditionNodes.size(); i++) {
            Node<AbstractCondition> node = visibleConditionNodes.get(i);
            final AbstractCondition condition = node.getData();
            BoxLayout labelAndOperationCellContent = null;
            Component paramEditComponentCellContent = null;
            Component groupCellContent = null;
            if (condition.isGroup()) {
                groupCellContent = createGroupConditionBox(condition, node, conditionsFocusType, currentFocusSet,
                        level);
                level++;
            } else {
                if (condition.getParam().getJavaClass() != null) {
                    ParamEditor paramEditor = createParamEditor(condition);

                    if (firstParamEditor == null)
                        firstParamEditor = paramEditor;
                    lastParamEditor = paramEditor;
                    currentFocusSet = true;

                    labelAndOperationCellContent = paramEditor.getLabelAndOperationLayout();
                    paramEditComponentCellContent = paramEditor.getParamEditComponentLayout();
                } else {
                    BoxLayout paramLayout = componentsFactory.createComponent(HBoxLayout.class);
                    paramLayout.setSpacing(true);
                    paramLayout.setMargin(false);

                    labelAndOperationCellContent = paramLayout;
                }
            }

            //groupBox for group conditions must occupy the whole line in conditions grid
            Integer conditionWidth = condition.isGroup() ? (Integer) conditionsCount : condition.getWidth();
            int nextColumnEnd = nextColumnStart + conditionWidth - 1;
            if (nextColumnEnd >= conditionsCount) {
                //complete current row in grid with gaps if next cell will be on next row
                completeGridRowWithGaps(grid, row, nextColumnStart, false);
                //place cell to next row in grid
                nextColumnStart = 0;
                nextColumnEnd = conditionWidth - 1;
                row++;
                grid.setRows(row + 1);
            }

            if (groupCellContent != null) {
                grid.add(groupCellContent, nextColumnStart * 2, row, nextColumnEnd * 2 + 1, row);
            }
            if (labelAndOperationCellContent != null) {
                labelAndOperationCellContent.addStyleName("param-label-layout");
                grid.add(labelAndOperationCellContent, nextColumnStart * 2, row, nextColumnStart * 2, row);
                if (nextColumnStart != 0)
                    labelAndOperationCellContent.setMargin(false, false, false, true);
            }
            if (paramEditComponentCellContent != null) {
                paramEditComponentCellContent.addStyleName("param-field-layout");
                grid.add(paramEditComponentCellContent, nextColumnStart * 2 + 1, row, nextColumnEnd * 2 + 1, row);
            }

            nextColumnStart = nextColumnEnd + 1;

            //add next row if necessary
            if (i < visibleConditionNodes.size() - 1) {
                if (nextColumnStart >= conditionsCount) {
                    nextColumnStart = 0;
                    row++;
                    grid.setRows(row + 1);
                }
            }
        }

        if (!initialFocusSet) {
            switch (conditionsFocusType) {
            case FIRST:
                if (firstParamEditor != null)
                    paramEditComponentToFocus = firstParamEditor;
                break;
            case LAST:
                if (lastParamEditor != null)
                    paramEditComponentToFocus = lastParamEditor;
            }
        }

        //complete last row in grid with gaps
        completeGridRowWithGaps(grid, row, nextColumnStart, true);

        if (parentContainer != null) {
            parentContainer.add(grid);
        }

        if (level == 0)
            controlsLayout.setStyleName(getControlsLayoutStyleName());
    }

    protected List<Node<AbstractCondition>> fetchVisibleNodes(List<Node<AbstractCondition>> nodes) {
        List<Node<AbstractCondition>> visibleConditionNodes = new ArrayList<>();
        for (Node<AbstractCondition> node : nodes) {
            AbstractCondition condition = node.getData();
            if (!condition.getHidden())
                visibleConditionNodes.add(node);
        }
        return visibleConditionNodes;
    }

    protected Component createGroupConditionBox(AbstractCondition condition, Node<AbstractCondition> node,
            ConditionsFocusType conditionsFocusType, boolean focusSet, int level) {
        Component groupCellContent;
        GroupBoxLayout groupBox = componentsFactory.createComponent(GroupBoxLayout.class);
        groupBox.setStyleName("conditions-group");
        groupBox.setWidth("100%");
        groupBox.setCaption(condition.getLocCaption());

        if (!node.getChildren().isEmpty()) {
            recursivelyCreateConditionsLayout(conditionsFocusType, focusSet, node.getChildren(), groupBox, level);
        }
        groupCellContent = groupBox;
        return groupCellContent;
    }

    protected ParamEditor createParamEditor(final AbstractCondition condition) {
        boolean conditionRemoveEnabled = !initialConditions.contains(condition);
        ParamEditor paramEditor = new ParamEditor(condition, conditionRemoveEnabled,
                isParamEditorOperationEditable());
        AbstractAction removeConditionAction = new AbstractAction("") {
            @Override
            public void actionPerform(Component component) {
                conditions.removeCondition(condition);
                fillConditionsLayout(ConditionsFocusType.NONE);
                updateFilterModifiedIndicator();
            }
        };
        removeConditionAction.setVisible(conditionRemoveEnabled);
        paramEditor.setRemoveButtonAction(removeConditionAction);
        return paramEditor;
    }

    protected boolean isParamEditorOperationEditable() {
        return isEditable() && userCanEditFilers();
    }

    /**
     * Adds empty containers to grid row. If not to complete the row with gaps then in case of grid with one element (element width = 1)
     * this element will occupy 100% of grid width, but expected behaviour is to occupy 1/3 of grid width
     */
    protected void completeGridRowWithGaps(GridLayout grid, int row, int startColumn, boolean lastRow) {
        for (int i = startColumn * 2; i < grid.getColumns(); i++) {
            Component gap = componentsFactory.createComponent(Label.class);
            gap.setWidth("100%");
            grid.add(gap, i, row);
        }
    }

    protected String getControlsLayoutStyleName() {
        String styleName = "filter-control-no-border";
        if (conditionsLayout.isVisible() && !conditionsLayout.getComponents().isEmpty()) {
            styleName = CONDITIONS_LOCATION_TOP.equals(conditionsLocation) ? "filter-control-with-top-border"
                    : "filter-control-with-bottom-border";
        }
        return styleName;
    }

    protected boolean isFilterModified() {
        boolean filterPropertiesModified = !Objects.equals(initialFilterEntity.getName(), filterEntity.getName())
                || !Objects.equals(initialFilterEntity.getCode(), filterEntity.getCode())
                || !Objects.equals(initialFilterEntity.getUser(), filterEntity.getUser());
        if (filterPropertiesModified)
            return true;
        String filterXml = filterEntity.getFolder() == null
                ? filterParser.getXml(conditions, Param.ValueProperty.DEFAULT_VALUE)
                : filterParser.getXml(conditions, Param.ValueProperty.VALUE);
        return !Objects.equals(filterXml, initialFilterEntity.getXml());
    }

    protected void updateFilterModifiedIndicator() {
        boolean filterModified = isFilterModified();
        saveAction.setEnabled(filterSavingPossible && filterModified);
        saveWithValuesAction.setEnabled(filterSavingPossible);

        String currentCaption = groupBoxLayout.getCaption();
        if (StringUtils.isEmpty(currentCaption))
            return;

        if (filterModified && !currentCaption.endsWith(MODIFIED_INDICATOR_SYMBOL)) {
            groupBoxLayout.setCaption(currentCaption + MODIFIED_INDICATOR_SYMBOL);
        }
        if (!filterModified && currentCaption.endsWith(MODIFIED_INDICATOR_SYMBOL)) {
            groupBoxLayout.setCaption(currentCaption.substring(0, currentCaption.length() - 1));
        }
    }

    /**
     * Load filter entities from database and saves them in {@code filterEntities} collection.
     */
    protected void loadFilterEntities() {
        LoadContext<FilterEntity> ctx = LoadContext.create(FilterEntity.class);
        ctx.setView("app");
        ctx.setQueryString("select f from sec$Filter f left join f.user u "
                + "where f.componentId = :component and (u.id = :userId or u is null) order by f.name")
                .setParameter("component", ComponentsHelper.getFilterComponentPath(filter))
                .setParameter("userId", userSessionSource.getUserSession().getCurrentOrSubstitutedUser().getId());

        filterEntities = new ArrayList<>(dataService.loadList(ctx));
    }

    protected FilterEntity getDefaultFilter(List<FilterEntity> filters) {
        Window window = ComponentsHelper.getWindow(filter);

        // First check if there is parameter with name equal to this filter component id, containing a filter code to apply
        Map<String, Object> params = filter.getFrame().getContext().getParams();
        String code = (String) params.get(filter.getId());
        if (!StringUtils.isBlank(code)) {
            for (FilterEntity filter : filters) {
                if (code.equals(filter.getCode()))
                    return filter;
            }
        }

        // No 'filter' parameter found, load default filter
        SettingsImpl settings = new SettingsImpl(window.getId());

        String componentPath = ComponentsHelper.getFilterComponentPath(filter);
        String[] strings = ValuePathHelper.parse(componentPath);
        String name = ValuePathHelper.format((String[]) ArrayUtils.subarray(strings, 1, strings.length));

        Element e = settings.get(name).element("defaultFilter");
        if (e != null) {
            String defIdStr = e.attributeValue("id");
            Boolean applyDefault = Boolean.valueOf(e.attributeValue("applyDefault"));
            if (!StringUtils.isBlank(defIdStr)) {
                UUID defaultId = null;
                try {
                    defaultId = UUID.fromString(defIdStr);
                } catch (IllegalArgumentException ex) {
                    //
                }
                if (defaultId != null) {
                    for (FilterEntity filter : filters) {
                        if (defaultId.equals(filter.getId())) {
                            filter.setIsDefault(true);
                            filter.setApplyDefault(applyDefault);
                            return filter;
                        }
                    }
                }
            }
        }

        FilterEntity globalDefaultFilter = filters.stream()
                .filter(filterEntity -> Boolean.TRUE.equals(filterEntity.getGlobalDefault())).findAny()
                .orElse(null);
        return globalDefaultFilter;
    }

    protected void initFiltersPopupButton() {
        filtersPopupButton.removeAllActions();
        addFiltersPopupActions();
    }

    protected void addFiltersPopupActions() {
        addResetFilterAction(filtersPopupButton);

        filterEntities.sort(Comparator.comparing(this::getFilterCaption));

        Iterator<FilterEntity> it = filterEntities.iterator();
        int addedEntitiesCount = 0;
        while (it.hasNext() && addedEntitiesCount < clientConfig.getGenericFilterPopupListSize()) {
            final FilterEntity fe = it.next();
            addSetFilterEntityAction(filtersPopupButton, fe);
            addedEntitiesCount++;
        }

        if (filterEntities.size() > clientConfig.getGenericFilterPopupListSize()) {
            addShowMoreFilterEntitiesAction(filtersPopupButton);
        }
    }

    protected void addSetFilterEntityAction(PopupButton popupButton, final FilterEntity fe) {
        popupButton.addAction(new AbstractAction("setEntity" + fe.getId()) {
            @Override
            public void actionPerform(Component component) {
                if (fe != filterEntity) {
                    setFilterEntity(fe);
                }
            }

            @Override
            public String getCaption() {
                return getFilterCaption(fe);
            }
        });
    }

    protected void addShowMoreFilterEntitiesAction(PopupButton popupButton) {
        popupButton.addAction(new AbstractAction("showMoreFilterEntities") {
            @Override
            public void actionPerform(Component component) {
                WindowInfo windowInfo = windowConfig.getWindowInfo("filterSelect");
                FilterSelectWindow window = (FilterSelectWindow) windowManager.openWindow(windowInfo,
                        WindowManager.OpenType.DIALOG, ParamsMap.of("filterEntities", filterEntities));

                window.addCloseListener(actionId -> {
                    if (Window.COMMIT_ACTION_ID.equals(actionId)) {
                        FilterEntity selectedEntity = window.getFilterEntity();
                        setFilterEntity(selectedEntity);
                    }
                });
            }

            @Override
            public String getCaption() {
                return formatMainMessage("filter.showMore", filterEntities.size());
            }
        });
    }

    protected void addResetFilterAction(PopupButton popupButton) {
        popupButton.addAction(new AbstractAction("resetFilter") {
            @Override
            public void actionPerform(Component component) {
                conditions = new ConditionsTree();
                setFilterEntity(adHocFilter);
            }

            @Override
            public String getCaption() {
                return getMainMessage("filter.resetFilter");
            }
        });
    }

    protected void initFiltersLookup() {
        Map<Object, String> captionsMap = new LinkedHashMap<>();
        for (FilterEntity entity : filterEntities) {
            String caption = getFilterCaption(entity);
            if (entity.getIsDefault()) {
                caption += " " + getMainMessage("filter.default");
            }
            captionsMap.put(entity, caption);
        }
        captionsMap.put(adHocFilter, getFilterCaption(adHocFilter));
        filtersLookupListenerEnabled = false;
        //set null to remove previous value from lookup options list
        filtersLookup.setValue(null);
        List<Object> optionsList = new ArrayList<>();
        optionsList.add(adHocFilter);
        optionsList.addAll(filterEntities);
        filtersLookup.setOptionsList(optionsList);
        filterHelper.setLookupCaptions(filtersLookup, captionsMap);
        filtersLookup.setValue(filterEntity);
        filtersLookupListenerEnabled = true;
    }

    protected void initFilterSelectComponents() {
        if (filtersPopupDisplayed) {
            initFiltersPopupButton();
        }
        if (filtersLookupDisplayed) {
            initFiltersLookup();
        }
    }

    protected void initAdHocFilter() {
        adHocFilter = metadata.create(FilterEntity.class);
        String emptyXml = filterParser.getXml(new ConditionsTree(), Param.ValueProperty.VALUE);
        adHocFilter.setXml(emptyXml);
        adHocFilter.setComponentId(ComponentsHelper.getFilterComponentPath(filter));
        adHocFilter.setUser(userSessionSource.getUserSession().getCurrentOrSubstitutedUser());
        adHocFilter.setName(getMainMessage("filter.adHocFilter"));
    }

    protected void addAppliedFilter() {
        if (lastAppliedFilter == null)
            return;

        if (!appliedFilters.isEmpty() && appliedFilters.getLast().filter.equals(lastAppliedFilter))
            return;

        this.layout.add(appliedFiltersLayout, CONDITIONS_LOCATION_TOP.equals(conditionsLocation) ? 0 : 1);

        BoxLayout layout = componentsFactory.createComponent(HBoxLayout.class);
        layout.setSpacing(true);

        if (!appliedFilters.isEmpty()) {
            AppliedFilterHolder holder = appliedFilters.getLast();
            holder.layout.remove(holder.button);
        }

        Label label = componentsFactory.createComponent(Label.class);
        label.setValue(lastAppliedFilter.getText());
        layout.add(label);
        label.setAlignment(Alignment.MIDDLE_LEFT);

        LinkButton button = componentsFactory.createComponent(LinkButton.class);
        button.setIcon("icons/item-remove.png");
        button.setAction(new AbstractAction("") {
            @Override
            public void actionPerform(Component component) {
                removeAppliedFilter();
            }
        });
        layout.add(button);

        addAppliedFilterLayoutHook(layout);
        appliedFiltersLayout.add(layout);

        appliedFilters.add(new AppliedFilterHolder(lastAppliedFilter, layout, button));
    }

    protected void addAppliedFilterLayoutHook(Component.Container layout) {
        //nothing
    }

    protected void removeAppliedFilter() {
        if (!appliedFilters.isEmpty()) {
            if (appliedFilters.size() == 1) {
                AppliedFilterHolder holder = appliedFilters.removeLast();
                appliedFiltersLayout.remove(holder.layout);
                ((CollectionDatasource.SupportsApplyToSelected) datasource).unpinAllQuery();
                this.layout.remove(appliedFiltersLayout);
            } else {
                windowManager.showOptionDialog(messages.getMainMessage("removeApplied.title"),
                        messages.getMainMessage("removeApplied.message"), MessageType.WARNING,
                        new Action[] { new DialogAction(Type.YES).withHandler(event -> {
                            for (AppliedFilterHolder holder : appliedFilters) {
                                appliedFiltersLayout.remove(holder.layout);
                                FilterDelegateImpl.this.layout.remove(appliedFiltersLayout);
                            }
                            appliedFilters.clear();
                            ((CollectionDatasource.SupportsApplyToSelected) datasource).unpinAllQuery();
                        }), new DialogAction(Type.NO, Status.PRIMARY) });
            }
        }
    }

    protected String getFilterCaption(FilterEntity filterEntity) {
        String name;
        if (filterEntity != null) {
            if (filterEntity.getCode() == null)
                name = InstanceUtils.getInstanceName(filterEntity);
            else {
                name = messages.getMainMessage(filterEntity.getCode());
            }
            AbstractSearchFolder folder = filterEntity.getFolder();
            if (folder != null) {
                if (!StringUtils.isBlank(folder.getTabName()))
                    name = messages.getMainMessage(folder.getTabName());
                else if (!StringUtils.isBlank(folder.getName())) {
                    name = messages.getMainMessage(folder.getName());
                }
                if (BooleanUtils.isTrue(filterEntity.getIsSet()))
                    name = getMainMessage("filter.setPrefix") + " " + name;
                else
                    name = getMainMessage("filter.folderPrefix") + " " + name;
            }
        } else
            name = "";
        return name;
    }

    protected String formatMainMessage(String key, Object... params) {
        return messages.formatMainMessage(key, params);
    }

    protected String getMainMessage(String key) {
        return messages.getMainMessage(key);
    }

    @Override
    public Component.Container getLayout() {
        return groupBoxLayout;
    }

    @Override
    public void setDatasource(CollectionDatasource datasource) {
        this.datasource = datasource;
        this.dsQueryFilter = datasource.getQueryFilter();

        if (getResultingManualApplyRequired()) {
            // set initial denying condition to get empty datasource before explicit filter applying
            QueryFilter queryFilter = new QueryFilter(new DenyingClause());
            if (dsQueryFilter != null) {
                queryFilter = QueryFilter.merge(dsQueryFilter, queryFilter);
            }
            datasource.setQueryFilter(queryFilter);
        }

        if (datasource instanceof CollectionDatasource.Lazy || datasource instanceof HierarchicalDatasource) {
            setUseMaxResults(false);
        } else if (useMaxResults) {
            initMaxResults();
        }

        if (!isFtsModeEnabled()) {
            controlsLayout.remove(ftsSwitch);
        }
    }

    @Override
    public CollectionDatasource getDatasource() {
        return datasource;
    }

    protected void initMaxResults() {
        int maxResults;
        if (this.maxResults != -1) {
            maxResults = this.maxResults;
        } else {
            maxResults = datasource.getMaxResults();
        }

        if (maxResults == 0
                || maxResults == persistenceManager.getMaxFetchUI(datasource.getMetaClass().getName())) {
            maxResults = persistenceManager.getFetchUI(datasource.getMetaClass().getName());
        }

        if (maxResultsAddedToLayout) {
            if (!textMaxResults) {
                List<Integer> optionsList = ((LookupField) maxResultsField).getOptionsList();
                if (!optionsList.contains(maxResults)) {
                    maxResults = findClosestValue(maxResults, optionsList);

                    Collections.sort(optionsList);
                    ((LookupField) maxResultsField).setOptionsList(optionsList);
                }
            }
            maxResultsField.setValue(maxResults);
        }

        datasource.setMaxResults(maxResults);
    }

    protected int findClosestValue(int maxResults, List<Integer> optionsList) {
        int minimumValue = Integer.MAX_VALUE;
        int closest = maxResults;

        for (int option : optionsList) {
            int diff = Math.abs(option - maxResults);
            if (diff < minimumValue) {
                minimumValue = diff;
                closest = option;
            }
        }

        return closest;
    }

    protected boolean isFtsModeEnabled() {
        return FtsConfigHelper.getEnabled() && ftsFilterHelper != null && datasource != null
                && ftsFilterHelper.isEntityIndexed(datasource.getMetaClass().getName())
                && Stores.isMain(metadata.getTools().getStoreName(datasource.getMetaClass()));
    }

    @Override
    public int getMaxResults() {
        return maxResults;
    }

    @Override
    public void setMaxResults(int maxResults) {
        this.maxResults = maxResults;
        initMaxResults();
    }

    @Override
    public void setUseMaxResults(boolean useMaxResults) {
        this.useMaxResults = useMaxResults;
        if (maxResultsLayout != null)
            maxResultsLayout.setVisible(isMaxResultsLayoutVisible());
    }

    protected boolean isMaxResultsLayoutVisible() {
        return useMaxResults && security.isSpecificPermitted("cuba.gui.filter.maxResults")
                && maxResultsAddedToLayout;
    }

    @Override
    public boolean getUseMaxResults() {
        return useMaxResults;
    }

    @Override
    public void setTextMaxResults(boolean textMaxResults) {
        boolean valueChanged = this.textMaxResults != textMaxResults;
        this.textMaxResults = textMaxResults;
        if (maxResultsAddedToLayout && valueChanged) {
            maxResultsLayout.remove(maxResultsField);
            maxResultsField = textMaxResults ? maxResultsTextField : maxResultsLookupField;
            maxResultsLayout.add(maxResultsField);
        }
    }

    @Override
    public boolean getTextMaxResults() {
        return textMaxResults;
    }

    @Override
    public boolean apply(boolean isNewWindow) {
        if (beforeFilterAppliedHandler != null) {
            if (!beforeFilterAppliedHandler.beforeFilterApplied())
                return false;
        }
        if (clientConfig.getGenericFilterChecking()) {
            if (filterEntity != null && conditions.getRoots().size() > 0) {
                boolean haveCorrectCondition = hasCorrectCondition();
                if (!haveCorrectCondition) {
                    if (!isNewWindow) {
                        windowManager.showNotification(messages.getMainMessage("filter.emptyConditions"),
                                Frame.NotificationType.HUMANIZED);
                    }
                    return false;
                }
            }
        }

        if (filterEntity != null) {
            lastAppliedFilter = new AppliedFilter(filterEntity, conditions);
        } else {
            lastAppliedFilter = null;
        }

        if (filterEntity != null) {
            boolean haveRequiredConditions = haveFilledRequiredConditions();
            if (!haveRequiredConditions) {
                if (!isNewWindow) {
                    windowManager.showNotification(messages.getMainMessage("filter.emptyRequiredConditions"),
                            Frame.NotificationType.HUMANIZED);
                }
                return false;
            }
            setFilterActionsEnabled();
        }

        applyDatasourceFilter();
        initDatasourceMaxResults();

        Map<String, Object> parameters = prepareDatasourceCustomParams();
        refreshDatasource(parameters);

        if ((applyTo != null) && (Table.class.isAssignableFrom(applyTo.getClass()))) {
            filterHelper.removeTableFtsTooltips((Table) applyTo);
        }

        if (afterFilterAppliedHandler != null) {
            afterFilterAppliedHandler.afterFilterApplied();
        }
        return true;
    }

    protected Map<String, Object> prepareDatasourceCustomParams() {
        Map<String, Object> lastRefreshParameters = new HashMap<>(datasource.getLastRefreshParameters());
        for (String paramName : ftsLastDatasourceRefreshParamsNames) {
            lastRefreshParameters.remove(paramName);
        }
        List<FtsCondition> ftsConditions = conditions.toConditionsList().stream()
                .filter(abstractCondition -> abstractCondition instanceof FtsCondition)
                .map(abstractCondition -> (FtsCondition) abstractCondition).collect(Collectors.toList());

        for (FtsCondition ftsCondition : ftsConditions) {
            String searchTerm = (String) ftsCondition.getParam().getValue();
            if (!Strings.isNullOrEmpty(searchTerm)) {
                FtsFilterHelper.FtsSearchResult ftsSearchResult = ftsFilterHelper.search(searchTerm,
                        datasource.getMetaClass().getName());
                int queryKey = ftsSearchResult.getQueryKey();

                lastRefreshParameters.put(ftsCondition.getSessionIdParamName(),
                        userSessionSource.getUserSession().getId());
                lastRefreshParameters.put(ftsCondition.getQueryKeyParamName(), queryKey);

                ftsLastDatasourceRefreshParamsNames.add(ftsCondition.getSessionIdParamName());
                ftsLastDatasourceRefreshParamsNames.add(ftsCondition.getQueryKeyParamName());
            }
        }
        return lastRefreshParameters;
    }

    protected void applyFts() {
        if (ftsFilterHelper == null)
            return;

        if (beforeFilterAppliedHandler != null) {
            if (!beforeFilterAppliedHandler.beforeFilterApplied())
                return;
        }

        String searchTerm = ftsSearchCriteriaField.getValue();
        if (Strings.isNullOrEmpty(searchTerm) && clientConfig.getGenericFilterChecking()) {
            windowManager.showNotification(getMainMessage("filter.fillSearchCondition"),
                    Frame.NotificationType.TRAY);
            return;
        }

        Map<String, Object> params = new HashMap<>();

        if (!Strings.isNullOrEmpty(searchTerm)) {
            FtsFilterHelper.FtsSearchResult ftsSearchResult = ftsFilterHelper.search(searchTerm,
                    datasource.getMetaClass().getName());
            int queryKey = ftsSearchResult.getQueryKey();
            params.put(FtsFilterHelper.SESSION_ID_PARAM_NAME, userSessionSource.getUserSession().getId());
            params.put(FtsFilterHelper.QUERY_KEY_PARAM_NAME, queryKey);

            CustomCondition ftsCondition = ftsFilterHelper.createFtsCondition(datasource.getMetaClass().getName());
            conditions = new ConditionsTree();
            conditions.getRootNodes().add(new Node<>(ftsCondition));

            if ((applyTo != null) && (Table.class.isAssignableFrom(applyTo.getClass()))) {
                filterHelper.initTableFtsTooltips((Table) applyTo, ftsSearchResult.getHitInfos());
            }
        } else if ((applyTo != null) && (Table.class.isAssignableFrom(applyTo.getClass()))) {
            filterHelper.initTableFtsTooltips((Table) applyTo, Collections.emptyMap());
        }

        applyDatasourceFilter();
        initDatasourceMaxResults();
        datasource.refresh(params);

        if (afterFilterAppliedHandler != null) {
            afterFilterAppliedHandler.afterFilterApplied();
        }
    }

    /**
     * The method is invoked before search. It sets datasource {@code maxResults} value based on the maxResults
     * field (if visible) or on maxFetchUI value.
     * Method also resets the datasource {@code firstResult} value
     */
    protected void initDatasourceMaxResults() {
        if (datasource == null) {
            throw new DevelopmentException("Filter datasource is not set");
        }
        if (this.maxResults != -1) {
            datasource.setMaxResults(maxResults);
        } else if (maxResultsAddedToLayout && useMaxResults) {
            Integer maxResults = maxResultsField.getValue();
            if (maxResults != null && maxResults > 0) {
                datasource.setMaxResults(maxResults);
            } else {
                datasource.setMaxResults(persistenceManager.getFetchUI(datasource.getMetaClass().getName()));
            }
        }
        if (datasource instanceof CollectionDatasource.SupportsPaging) {
            ((CollectionDatasource.SupportsPaging) datasource).setFirstResult(0);
        }
    }

    protected void applyDatasourceFilter() {
        if (datasource != null) {

            String currentFilterXml = filterParser.getXml(conditions, Param.ValueProperty.VALUE);

            if (!Strings.isNullOrEmpty(currentFilterXml)) {
                Element element = Dom4j.readDocument(currentFilterXml).getRootElement();
                QueryFilter queryFilter = new QueryFilter(element);

                if (dsQueryFilter != null) {
                    queryFilter = QueryFilter.merge(dsQueryFilter, queryFilter);
                }

                datasource.setQueryFilter(queryFilter);
            } else {
                datasource.setQueryFilter(dsQueryFilter);
            }
        } else {
            log.warn("Unable to apply datasource filter with null datasource");
        }
    }

    protected boolean haveFilledRequiredConditions() {
        for (AbstractCondition condition : conditions.toConditionsList()) {
            if ((condition.getRequired()) && (condition.getParam() != null)
                    && (condition.getParam().getValue() == null)) {
                return false;
            }
        }
        return true;
    }

    protected boolean hasCorrectCondition() {
        boolean haveCorrectCondition = false;

        for (AbstractCondition condition : conditions.toConditionsList()) {
            if ((condition.getParam() != null) && (condition.getParam().getValue() != null)
                    || condition instanceof CustomCondition && condition.getHidden()) {
                haveCorrectCondition = true;
                break;
            }
        }
        return haveCorrectCondition;
    }

    /**
     * extenders should be able to modify the datasource
     * before it will be refreshed
     */
    protected void refreshDatasource(Map<String, Object> parameters) {
        if (datasource instanceof CollectionDatasource.Suspendable)
            ((CollectionDatasource.Suspendable) datasource).refreshIfNotSuspended(parameters);
        else
            datasource.refresh(parameters);
    }

    @Override
    public String getCaption() {
        return caption;
    }

    @Override
    public void setCaption(String caption) {
        this.caption = caption;
        groupBoxLayout.setCaption(caption);
    }

    @Override
    public String getIcon() {
        return groupBoxLayout.getIcon();
    }

    @Override
    public void setIcon(String icon) {
        groupBoxLayout.setIcon(icon);
    }

    @Override
    public void setManualApplyRequired(Boolean manualApplyRequired) {
        this.manualApplyRequired = manualApplyRequired;
    }

    @Override
    public Boolean getManualApplyRequired() {
        return manualApplyRequired;
    }

    protected boolean getResultingManualApplyRequired() {
        return manualApplyRequired != null ? manualApplyRequired
                : clientConfig.getGenericFilterManualApplyRequired();
    }

    @Override
    public Component getOwnComponent(String id) {
        for (AbstractCondition condition : conditions.toConditionsList()) {
            if (condition.getParam() != null) {
                String paramName = condition.getParam().getName();

                String componentName = paramName.substring(paramName.lastIndexOf('.') + 1);
                if (id.equals(componentName)) {
                    ParamWrapper wrapper = new ParamWrapper(condition, condition.getParam());
                    return wrapper;
                }
            }
        }
        return null;
    }

    @Override
    @Nullable
    public Component getComponent(String id) {
        String[] elements = ValuePathHelper.parse(id);
        if (elements.length == 1) {
            return getOwnComponent(id);
        } else {
            throw new UnsupportedOperationException("Filter contains only one level of sub components");
        }
    }

    @Override
    public void applySettings(Element element) {
        Element groupBoxExpandedEl = element.element("groupBoxExpanded");
        if (groupBoxExpandedEl != null) {
            Boolean expanded = Boolean.valueOf(groupBoxExpandedEl.getText());
            groupBoxLayout.setExpanded(expanded);
        }
    }

    @Override
    public boolean saveSettings(Element element) {
        Boolean changed = false;
        Element e = element.element("defaultFilter");
        if (e == null)
            e = element.addElement("defaultFilter");

        UUID defaultId = null;
        Boolean applyDefault = false;

        for (FilterEntity filter : filterEntities) {
            if (BooleanUtils.isTrue(filter.getIsDefault())) {
                defaultId = filter.getId();
                applyDefault = filter.getApplyDefault();
                break;
            }
        }

        String newDef = defaultId != null ? defaultId.toString() : null;
        Attribute attr = e.attribute("id");
        String oldDef = attr != null ? attr.getValue() : null;
        if (!Objects.equals(oldDef, newDef)) {
            if (newDef == null && attr != null) {
                e.remove(attr);
            } else {
                if (attr == null)
                    e.addAttribute("id", newDef);
                else
                    attr.setValue(newDef);
            }
            changed = true;
        }
        Boolean newApplyDef = BooleanUtils.isTrue(applyDefault);
        Attribute applyDefaultAttr = e.attribute("applyDefault");
        Boolean oldApplyDef = applyDefaultAttr != null ? Boolean.valueOf(applyDefaultAttr.getValue()) : false;
        if (!Objects.equals(oldApplyDef, newApplyDef)) {
            if (applyDefaultAttr != null) {
                applyDefaultAttr.setValue(newApplyDef.toString());
            } else {
                e.addAttribute("applyDefault", newApplyDef.toString());
            }
            changed = true;
        }

        Element groupBoxExpandedEl = element.element("groupBoxExpanded");
        if (groupBoxExpandedEl == null)
            groupBoxExpandedEl = element.addElement("groupBoxExpanded");

        Boolean oldGroupBoxExpandedValue = Boolean.valueOf(groupBoxExpandedEl.getText());
        Boolean newGroupBoxExpandedValue = groupBoxLayout.isExpanded();
        if (!Objects.equals(oldGroupBoxExpandedValue, newGroupBoxExpandedValue)) {
            groupBoxExpandedEl.setText(newGroupBoxExpandedValue.toString());
            changed = true;
        }

        return changed;
    }

    @Override
    public Component getApplyTo() {
        return applyTo;
    }

    @Override
    public void setApplyTo(Component applyTo) {
        this.applyTo = applyTo;
    }

    @Override
    public void setFolderActionsEnabled(boolean folderActionsEnabled) {
        this.folderActionsEnabled = folderActionsEnabled;
    }

    @Override
    public boolean isFolderActionsEnabled() {
        return folderActionsEnabled;
    }

    /**
     * Adds actions of 'Entities Set' functionality to Table component
     */
    protected void fillTableActions() {
        Table table;
        if ((applyTo != null) && (Table.class.isAssignableFrom(applyTo.getClass()))) {
            table = (Table) applyTo;
        } else {
            return;
        }
        ButtonsPanel buttons = table.getButtonsPanel();
        if (buttons == null) {
            return; // in lookup windows, there is no button panel
        }
        com.haulmont.cuba.gui.components.Button addToSetBtn = (Button) buttons.getComponent("addToSetBtn");
        com.haulmont.cuba.gui.components.Button addToCurSetBtn = (Button) buttons.getComponent("addToCurSetBtn");
        com.haulmont.cuba.gui.components.Button removeFromCurSetBtn = (Button) buttons
                .getComponent("removeFromCurSetBtn");

        Action addToSet = table.getAction("filter.addToSet");

        Action addToCurrSet = table.getAction("filter.addToCurSet");
        Action removeFromCurrSet = table.getAction("filter.removeFromCurSet");

        if (addToSet != null)
            table.removeAction(addToSet);
        if (addToSetBtn != null)
            addToSetBtn.setVisible(false);
        if (addToCurrSet != null) {
            table.removeAction(addToCurrSet);
        }
        if (addToCurSetBtn != null) {
            addToCurSetBtn.setVisible(false);
        }
        if (removeFromCurrSet != null) {
            table.removeAction(removeFromCurrSet);
        }
        if (removeFromCurSetBtn != null) {
            removeFromCurSetBtn.setVisible(false);
        }
        if ((filterEntity != null) && (BooleanUtils.isTrue(filterEntity.getIsSet()))) {
            addToCurrSet = new AddToCurrSetAction();

            if (addToCurSetBtn == null) {
                addToCurSetBtn = componentsFactory.createComponent(Button.class);
                addToCurSetBtn.setId("addToCurSetBtn");
                addToCurSetBtn.setCaption(getMainMessage("filter.addToCurSet"));
                buttons.add(addToCurSetBtn);
            } else {
                addToCurSetBtn.setVisible(true);
            }
            if (StringUtils.isEmpty(addToCurSetBtn.getIcon())) {
                addToCurSetBtn.setIcon("icons/join-to-set.png");
            }
            addToCurSetBtn.setAction(addToCurrSet);
            table.addAction(addToCurrSet);

            removeFromCurrSet = new RemoveFromSetAction(table);
            if (removeFromCurSetBtn == null) {
                removeFromCurSetBtn = componentsFactory.createComponent(Button.class);
                removeFromCurSetBtn.setId("removeFromCurSetBtn");
                removeFromCurSetBtn.setCaption(getMainMessage("filter.removeFromCurSet"));
                buttons.add(removeFromCurSetBtn);
            } else {
                removeFromCurSetBtn.setVisible(true);
            }
            if (StringUtils.isEmpty(removeFromCurSetBtn.getIcon())) {
                removeFromCurSetBtn.setIcon("icons/delete-from-set.png");
            }
            removeFromCurSetBtn.setAction(removeFromCurrSet);

            table.addAction(removeFromCurrSet);
        } else {
            addToSet = new AddToSetAction(table);
            if (addToSetBtn == null) {
                addToSetBtn = componentsFactory.createComponent(Button.class);
                addToSetBtn.setId("addToSetBtn");
                addToSetBtn.setCaption(getMainMessage("filter.addToSet"));
                buttons.add(addToSetBtn);
            } else {
                addToSetBtn.setVisible(true);
            }
            if (StringUtils.isEmpty(addToSetBtn.getIcon())) {
                addToSetBtn.setIcon("icons/insert-to-set.png");
            }
            addToSetBtn.setAction(addToSet);
            table.addAction(addToSet);
        }
    }

    @Override
    public void setEditable(boolean editable) {
        this.editable = editable;
        addConditionBtn.setVisible(editable && userCanEditFilers());
        //do not process actions if method is invoked from filter loader
        if (filterActionsCreated && filterEntity != null) {
            setFilterActionsEnabled();
            setFilterActionsVisible();
        }
    }

    @Override
    public boolean isEditable() {
        return editable;
    }

    @Override
    public Object getParamValue(String paramName) {
        Component component = getOwnComponent(paramName);
        if (component instanceof Component.HasValue) {
            return ((Component.HasValue) component).getValue();
        }
        return null;
    }

    @Override
    public void setParamValue(String paramName, Object value) {
        Component component = getOwnComponent(paramName);
        if (component instanceof Component.HasValue) {
            ((Component.HasValue) component).setValue(value);
        }
    }

    @Override
    public void addFilterEntityChangeListener(Filter.FilterEntityChangeListener listener) {
        filterEntityChangeListeners.add(listener);
    }

    @Override
    public List<Filter.FilterEntityChangeListener> getFilterEntityChangeListeners() {
        return filterEntityChangeListeners;
    }

    @Override
    public Integer getColumnsCount() {
        return columnsCount != null ? columnsCount : clientConfig.getGenericFilterColumnsCount();
    }

    @Override
    public void setColumnsCount(int columnsCount) {
        this.columnsCount = columnsCount;
    }

    @Override
    public boolean isExpanded() {
        return groupBoxLayout.isExpanded();
    }

    @Override
    public void setExpanded(boolean expanded) {
        groupBoxLayout.setExpanded(expanded);
    }

    @Override
    public boolean isCollapsable() {
        return groupBoxLayout.isCollapsable();
    }

    @Override
    public void setCollapsable(boolean collapsable) {
        groupBoxLayout.setCollapsable(collapsable);
    }

    @Override
    public void setModeSwitchVisible(boolean modeSwitchVisible) {
        this.modeSwitchVisible = modeSwitchVisible;
        ftsSwitch.setVisible(modeSwitchVisible && isFtsModeEnabled());
    }

    @Override
    public void requestFocus() {
        if (filterEntity == null) {
            delayedFocus = true;
            return;
        }
        if (paramEditComponentToFocus != null) {
            requestFocusToParamEditComponent();
        } else if (filtersLookupDisplayed) {
            filtersLookup.requestFocus();
        } else if (filtersPopupDisplayed) {
            filtersPopupButton.requestFocus();
        }
    }

    protected void requestFocusToParamEditComponent() {
        if (paramEditComponentToFocus instanceof ParamEditor) {
            ((ParamEditor) paramEditComponentToFocus).requestFocus();
        } else if (paramEditComponentToFocus instanceof TextField) {
            ((TextField) paramEditComponentToFocus).requestFocus();
        }
    }

    @Override
    public void addExpandedStateChangeListener(FDExpandedStateChangeListener listener) {
        if (expandedStateChangeListeners == null) {
            expandedStateChangeListeners = new ArrayList<>();
        }
        if (!expandedStateChangeListeners.contains(listener)) {
            expandedStateChangeListeners.add(listener);
        }
    }

    @Override
    public void removeExpandedStateChangeListener(FDExpandedStateChangeListener listener) {
        if (expandedStateChangeListeners != null) {
            expandedStateChangeListeners.remove(listener);
        }
    }

    protected void fireExpandStateChange() {
        if (expandedStateChangeListeners != null) {
            FDExpandedStateChangeEvent event = new FDExpandedStateChangeEvent(this, isExpanded());

            for (FDExpandedStateChangeListener listener : expandedStateChangeListeners) {
                listener.expandedStateChanged(event);
            }
        }
    }

    @Override
    public void setFilter(Filter filter) {
        this.filter = filter;
        addConditionHelper = new AddConditionHelper(filter, new AddConditionHelper.Handler() {
            @Override
            public void handle(AbstractCondition condition) {
                try {
                    addCondition(condition);
                } catch (Exception e) {
                    conditions.removeCondition(condition);
                    throw e;
                }
            }
        });
    }

    protected void addCondition(AbstractCondition condition) {
        conditions.getRootNodes().add(new Node<>(condition));
        fillConditionsLayout(ConditionsFocusType.LAST);
        requestFocusToParamEditComponent();
        updateFilterModifiedIndicator();
        condition.addListener(new AbstractCondition.Listener() {
            @Override
            public void captionChanged() {
            }

            @Override
            public void paramChanged(Param oldParam, Param newParam) {
                updateFilterModifiedIndicator();
            }
        });
    }

    protected void initShortcutActions() {
        if (filter.getFrame().getAction(Filter.APPLY_ACTION_ID) == null) {
            filter.getFrame()
                    .addAction(new AbstractAction(Filter.APPLY_ACTION_ID, clientConfig.getFilterApplyShortcut()) {
                        @Override
                        public void actionPerform(Component component) {
                            if (isVisible() && datasource != null) {
                                if (filterMode == FilterMode.GENERIC_MODE) {
                                    apply(false);
                                } else {
                                    applyFts();
                                }
                            }
                        }
                    });
        }

        if (filter.getFrame().getAction(Filter.SELECT_ACTION_ID) == null) {
            filter.getFrame()
                    .addAction(new AbstractAction(Filter.SELECT_ACTION_ID, clientConfig.getFilterSelectShortcut()) {
                        @Override
                        public void actionPerform(Component component) {
                            if (isVisible() && datasource != null && filtersPopupButton.isEnabled()) {
                                filtersPopupButton.setPopupVisible(true);
                            }
                        }
                    });
        }
    }

    protected void updateWindowCaption() {
        Window window = ComponentsHelper.getWindow(filter);
        String filterTitle;
        if (filterMode == FilterMode.GENERIC_MODE && filterEntity != null && filterEntity != adHocFilter) {
            filterTitle = getFilterCaption(filterEntity);
        } else {
            filterTitle = null;
        }
        window.setDescription(filterTitle);

        if (initialWindowCaption == null) {
            initialWindowCaption = window.getCaption();
        }

        windowManager.setWindowCaption(window, initialWindowCaption, filterTitle);

        groupBoxLayout.setCaption(Strings.isNullOrEmpty(filterTitle) ? caption : caption + ": " + filterTitle);
    }

    protected ControlsLayoutBuilder createControlsLayoutBuilder(String layoutDescription) {
        return new ControlsLayoutBuilder(layoutDescription);
    }

    /**
     * Method sets default = false to all filters except the current one
     */
    protected void resetDefaultFilters() {
        for (FilterEntity filter : filterEntities) {
            if (!Objects.equals(filter, filterEntity)) {
                if (BooleanUtils.isTrue(filter.getIsDefault())) {
                    filter.setIsDefault(false);
                }
            }
        }
    }

    @Override
    public Filter.BeforeFilterAppliedHandler getBeforeFilterAppliedHandler() {
        return beforeFilterAppliedHandler;
    }

    @Override
    public void setBeforeFilterAppliedHandler(Filter.BeforeFilterAppliedHandler beforeFilterAppliedHandler) {
        this.beforeFilterAppliedHandler = beforeFilterAppliedHandler;
    }

    @Override
    public Filter.AfterFilterAppliedHandler getAfterFilterAppliedHandler() {
        return afterFilterAppliedHandler;
    }

    @Override
    public void setAfterFilterAppliedHandler(Filter.AfterFilterAppliedHandler afterFilterAppliedHandler) {
        this.afterFilterAppliedHandler = afterFilterAppliedHandler;
    }

    protected class FiltersLookupChangeListener implements Component.ValueChangeListener {
        public FiltersLookupChangeListener() {
        }

        @Override
        public void valueChanged(Component.ValueChangeEvent e) {
            if (!filtersLookupListenerEnabled)
                return;
            if (e.getValue() instanceof FilterEntity) {
                setFilterEntity((FilterEntity) e.getValue());
            }
        }
    }

    protected class SaveAction extends AbstractAction {

        private final boolean saveWithValues;

        public SaveAction(String id, boolean saveWithValues, String caption) {
            super(id);
            this.saveWithValues = saveWithValues;
            this.caption = caption;
        }

        @Override
        public void actionPerform(Component component) {
            if (PersistenceHelper.isNew(filterEntity) && filterEntity.getFolder() == null) {
                WindowInfo windowInfo = windowConfig.getWindowInfo("saveFilter");
                Map<String, Object> params = new HashMap<>();
                if (!getMainMessage("filter.adHocFilter").equals(filterEntity.getName())) {
                    params.put("filterName", filterEntity.getName());
                }
                final SaveFilterWindow window = (SaveFilterWindow) windowManager.openWindow(windowInfo,
                        WindowManager.OpenType.DIALOG, params);
                window.addCloseListener(actionId -> {
                    if (Window.COMMIT_ACTION_ID.equals(actionId)) {
                        String filterName = window.getFilterName();
                        filterEntity.setName(filterName);

                        if (saveWithValues) {
                            conditions.toConditionsList().forEach(condition -> {
                                condition.getParam().setDefaultValue(condition.getParam().getValue());
                            });
                        }

                        filterEntity.setXml(filterParser.getXml(conditions, Param.ValueProperty.DEFAULT_VALUE));
                        saveFilterEntity();
                        initAdHocFilter();
                        initFilterSelectComponents();
                        updateWindowCaption();
                        //recreate layout to remove delete conditions buttons
                        initialConditions = conditions.toConditionsList();
                        fillConditionsLayout(ConditionsFocusType.NONE);
                    }
                    settingsBtn.requestFocus();
                });
            } else {
                if (saveWithValues) {
                    conditions.toConditionsList().forEach(condition -> {
                        condition.getParam().setDefaultValue(condition.getParam().getValue());
                    });
                }

                String xml = filterEntity.getFolder() == null
                        ? filterParser.getXml(conditions, Param.ValueProperty.DEFAULT_VALUE)
                        : filterParser.getXml(conditions, Param.ValueProperty.VALUE);
                filterEntity.setXml(xml);
                saveFilterEntity();
                initFilterSelectComponents();
            }
        }

        @Override
        public String getIcon() {
            return "icons/save.png";
        }
    }

    protected class SaveAsAction extends AbstractAction {

        protected SaveAsAction() {
            super("saveAs");
        }

        @Override
        public void actionPerform(Component component) {
            WindowInfo windowInfo = windowConfig.getWindowInfo("saveFilter");
            final SaveFilterWindow window = (SaveFilterWindow) windowManager.openWindow(windowInfo,
                    WindowManager.OpenType.DIALOG);
            window.addCloseListener(actionId -> {
                if (Window.COMMIT_ACTION_ID.equals(actionId)) {
                    String filterName = window.getFilterName();
                    FilterEntity newFilterEntity = metadata.create(FilterEntity.class);
                    metadata.getTools().copy(filterEntity, newFilterEntity);
                    newFilterEntity.setCode(null);
                    newFilterEntity.setId(UuidProvider.createUuid());
                    newFilterEntity.setGlobalDefault(false);
                    //if filter was global but current user cannot create global filter then new filter
                    //will be connected with current user
                    if (newFilterEntity.getUser() == null && !uerCanEditGlobalFilter()) {
                        newFilterEntity.setUser(userSessionSource.getUserSession().getCurrentOrSubstitutedUser());
                    }
                    String xml = filterEntity.getFolder() == null
                            ? filterParser.getXml(conditions, Param.ValueProperty.DEFAULT_VALUE)
                            : filterParser.getXml(conditions, Param.ValueProperty.VALUE);
                    filterEntity = newFilterEntity;
                    filterEntity.setName(filterName);
                    filterEntity.setXml(xml);
                    saveFilterEntity();
                    initFilterSelectComponents();
                    updateWindowCaption();

                    //recreate layout to remove delete conditions buttons
                    initialConditions = conditions.toConditionsList();
                    fillConditionsLayout(ConditionsFocusType.NONE);
                }
                settingsBtn.requestFocus();
            });
        }

        @Override
        public String getCaption() {
            return getMainMessage("filter.saveAs");
        }
    }

    protected class EditAction extends AbstractAction {

        protected EditAction() {
            super("edit");
        }

        @Override
        public void actionPerform(Component component) {
            WindowInfo windowInfo = windowConfig.getWindowInfo("filterEditor");
            Map<String, Object> params = new HashMap<>();
            params.put("filterEntity", filterEntity);
            params.put("filter", filter);
            params.put("conditions", conditions);

            FilterEditor window = (FilterEditor) windowManager.openWindow(windowInfo, WindowManager.OpenType.DIALOG,
                    params);
            window.addCloseListener(actionId -> {
                if (Window.COMMIT_ACTION_ID.equals(actionId)) {
                    conditions = window.getConditions();
                    filterEntity.setXml(filterParser.getXml(conditions, Param.ValueProperty.DEFAULT_VALUE));
                    if (filterEntity.getIsDefault()) {
                        resetDefaultFilters();
                    }
                    saveFilterEntity();
                    initAdHocFilter();
                    Set<FilterEntity> modifiedGlobalDefaultFilters = window.getModifiedGlobalDefaultFilters();
                    for (FilterEntity modifiedGlobalDefaultFilter : modifiedGlobalDefaultFilters) {
                        if (filterEntities.contains(modifiedGlobalDefaultFilter)) {
                            filterEntities.set(filterEntities.indexOf(modifiedGlobalDefaultFilter),
                                    modifiedGlobalDefaultFilter);
                        }
                    }
                    initFilterSelectComponents();
                    updateWindowCaption();
                    fillConditionsLayout(ConditionsFocusType.FIRST);
                    requestFocusToParamEditComponent();
                    updateFilterModifiedIndicator();
                } else {
                    requestFocusToParamEditComponent();
                }
                settingsBtn.requestFocus();
            });
        }

        @Override
        public String getCaption() {
            return getMainMessage("filter.edit");
        }

        @Override
        public String getIcon() {
            return "icons/edit.png";
        }
    }

    protected class MakeDefaultAction extends AbstractAction {

        public MakeDefaultAction() {
            super("filter.makeDefault");
        }

        @Override
        public void actionPerform(Component component) {
            setDefaultFilter();
        }

        protected void setDefaultFilter() {
            if (filterEntity != null) {
                filterEntity.setIsDefault(true);
            }
            resetDefaultFilters();
            initFilterSelectComponents();
            setFilterActionsEnabled();
        }

    }

    protected class RemoveAction extends AbstractAction {
        public RemoveAction() {
            super("remove");
        }

        @Override
        public void actionPerform(Component component) {
            if (filterEntity == adHocFilter)
                return;
            windowManager.showOptionDialog(getMainMessage("filter.removeDialogTitle"),
                    getMainMessage("filter.removeDialogMessage"), MessageType.CONFIRMATION,
                    new Action[] { new DialogAction(Type.YES).withHandler(event -> {
                        removeFilterEntity();
                        settingsBtn.requestFocus();
                    }), new DialogAction(Type.NO, Status.PRIMARY).withHandler(event -> {
                        settingsBtn.requestFocus();
                    }) });
        }

        @Override
        public String getCaption() {
            return getMainMessage("filter.remove");
        }

        @Override
        public String getIcon() {
            return "icons/remove.png";
        }
    }

    protected void removeFilterEntity() {
        CommitContext ctx = new CommitContext(Collections.emptyList(), Collections.singletonList(filterEntity));
        dataService.commit(ctx);
        filterEntities.remove(filterEntity);
        setFilterEntity(adHocFilter);
        initFilterSelectComponents();
        updateWindowCaption();
    }

    protected class PinAppliedAction extends AbstractAction {

        public PinAppliedAction() {
            super("pinApplied");
        }

        @Override
        public void actionPerform(Component component) {
            if (datasource instanceof CollectionDatasource.SupportsApplyToSelected) {
                ((CollectionDatasource.SupportsApplyToSelected) datasource).pinQuery();
                addAppliedFilter();

            }
        }

        @Override
        public String getCaption() {
            return getMainMessage("filter.pinApplied");
        }

        @Override
        public String getIcon() {
            return "icons/pin.png";
        }
    }

    protected class SaveAsFolderAction extends AbstractAction {

        public static final String SAVE_AS_APP_FOLDER = "saveAsAppFolder";
        public static final String SAVE_AS_SEARCH_FOLDER = "saveAsSearchFolder";

        protected boolean isAppFolder;

        protected SaveAsFolderAction(boolean isAppFolder) {
            super(isAppFolder ? SAVE_AS_APP_FOLDER : SAVE_AS_SEARCH_FOLDER);
            this.isAppFolder = isAppFolder;
        }

        @Override
        public String getCaption() {
            return getMainMessage("filter." + getId());
        }

        @Override
        public void actionPerform(Component component) {
            saveAsFolder(isAppFolder);
        }
    }

    protected class AddToSetAction extends ItemTrackingAction {
        protected AddToSetAction(Table table) {
            super(table, "filter.addToSet");
        }

        @Override
        public String getCaption() {
            return getMainMessage(getId());
        }

        @Override
        public void actionPerform(Component component) {
            Set<Entity> ownerSelection = target.getSelected();

            if (!ownerSelection.isEmpty()) {
                String entityType = target.getDatasource().getMetaClass().getName();
                Map<String, Object> params = new HashMap<>();
                params.put("entityType", entityType);
                params.put("items", ownerSelection);
                params.put("componentPath", ComponentsHelper.getFilterComponentPath(filter));
                String[] strings = ValuePathHelper.parse(ComponentsHelper.getFilterComponentPath(filter));
                String componentId = ValuePathHelper.format(Arrays.copyOfRange(strings, 1, strings.length));
                params.put("componentId", componentId);
                params.put("foldersPane", filterHelper.getFoldersPane());
                params.put("entityClass", datasource.getMetaClass().getJavaClass().getName());
                params.put("query", datasource.getQuery());
                filter.getFrame().openWindow("saveSetInFolder", WindowManager.OpenType.DIALOG, params);
            }
        }
    }

    protected class RemoveFromSetAction extends ItemTrackingAction {
        protected RemoveFromSetAction(Table table) {
            super(table, "filter.removeFromCurSet");
        }

        @Override
        public String getCaption() {
            return getMainMessage(getId());
        }

        @Override
        public void actionPerform(Component component) {
            if (filterEntity == null) {
                // todo add notification 'Filter not selected'
                return;
            }
            Set selected = target.getSelected();
            if (selected.isEmpty()) {
                return;
            }

            if (target.getDatasource().getItemIds().size() == 1) {
                filterHelper.removeFolderFromFoldersPane(filterEntity.getFolder());
                removeFilterEntity();

                Window window = ComponentsHelper.getWindow(filter);
                windowManager.close(window);
            } else {
                String filterXml = filterEntity.getXml();
                filterEntity.setXml(UserSetHelper.removeEntities(filterXml, selected));
                filterEntity.getFolder().setFilterXml(filterEntity.getXml());
                filterEntity.setFolder(saveFolder((filterEntity.getFolder())));
                setFilterEntity(filterEntity);
            }
        }
    }

    protected class AddToCurrSetAction extends BaseAction {

        protected AddToCurrSetAction() {
            super("filter.addToCurSet");
        }

        @Override
        public String getCaption() {
            return getMainMessage(getId());
        }

        @Override
        public void actionPerform(Component component) {
            if (filterEntity == null) {
                // todo add notification 'Filter not selected'
                return;
            }

            Frame frame = filter.getFrame();
            String[] strings = ValuePathHelper.parse(ComponentsHelper.getFilterComponentPath(filter));
            String windowAlias = strings[0];
            StringBuilder lookupAlias = new StringBuilder(windowAlias);
            if (windowAlias.endsWith(Window.BROWSE_WINDOW_SUFFIX)) {
                int index = lookupAlias.lastIndexOf(Window.BROWSE_WINDOW_SUFFIX);
                lookupAlias.delete(index, lookupAlias.length());
                lookupAlias.append(Window.LOOKUP_WINDOW_SUFFIX);
            }
            frame.openLookup(lookupAlias.toString(), new Window.Lookup.Handler() {

                @Override
                public void handleLookup(Collection items) {
                    String filterXml = filterEntity.getXml();
                    filterEntity.setXml(UserSetHelper.addEntities(filterXml, items));
                    filterEntity.getFolder().setFilterXml(filterEntity.getXml());
                    filterEntity.setFolder(saveFolder(filterEntity.getFolder()));
                    setFilterEntity(filterEntity);
                }
            }, WindowManager.OpenType.THIS_TAB);
        }
    }

    protected static class AppliedFilterHolder {
        public final AppliedFilter filter;
        public final Component.Container layout;
        public final Button button;

        protected AppliedFilterHolder(AppliedFilter filter, Component.Container layout, Button button) {
            this.filter = filter;
            this.layout = layout;
            this.button = button;
        }
    }

    /**
     * Class creates filter controls layout based on template.
     * See template format in documentation for {@link ClientConfig#getGenericFilterControlsLayout()}
     */
    protected class ControlsLayoutBuilder {

        protected Map<String, List<String>> components = new LinkedHashMap<>();
        protected Map<String, AbstractAction> filterActions = new HashMap<>();

        public ControlsLayoutBuilder(String layoutDescription) {
            initFilterActions();
            parseLayoutDescription(layoutDescription);
        }

        protected void initFilterActions() {
            filterActions.put("save", saveAction);
            filterActions.put("save_with_values", saveWithValuesAction);
            filterActions.put("save_as", saveAsAction);
            filterActions.put("edit", editAction);
            filterActions.put("remove", removeAction);
            filterActions.put("pin", pinAppliedAction);
            filterActions.put("save_search_folder", saveAsSearchFolderAction);
            filterActions.put("save_app_folder", saveAsAppFolderAction);
            filterActions.put("make_default", makeDefaultAction);
        }

        protected void parseLayoutDescription(String layoutDescription) {
            Pattern panelComponentPattern = Pattern.compile("\\[(.*?)\\]");
            Matcher matcher = panelComponentPattern.matcher(layoutDescription);
            Splitter componentDescriptionSplitter = Splitter.on("|").trimResults();
            Splitter optionsSplitter = Splitter.on(",").trimResults();
            while (matcher.find()) {
                String componentDescription = matcher.group(1);
                Iterable<String> parts = componentDescriptionSplitter.split(componentDescription);
                Iterator<String> iterator = parts.iterator();
                String componentName = iterator.next();
                String componentOptions = iterator.hasNext() ? iterator.next() : "";
                Iterable<String> options = optionsSplitter.split(componentOptions);
                components.put(componentName, Lists.newArrayList(options));
            }
        }

        public void build() {
            for (Map.Entry<String, List<String>> entry : components.entrySet()) {
                Component component = getControlsLayoutComponent(entry.getKey(), entry.getValue());
                if (component == null) {
                    log.warn("Filter controls layout component {} not supported", entry.getKey());
                    continue;
                }
                controlsLayout.add(component);
                if (component == controlsLayoutGap) {
                    controlsLayout.expand(component);
                }
            }
        }

        @Nullable
        protected Component getControlsLayoutComponent(String name, List<String> options) {
            switch (name) {
            case "filters_popup":
                filtersPopupDisplayed = true;
                return filtersPopupBox;
            case "filters_lookup":
                filtersLookupDisplayed = true;
                return filtersLookup;
            case "search":
                searchBtn.setParent(null);
                return searchBtn;
            case "add_condition":
                return addConditionBtn;
            case "settings":
                fillSettingsBtn(options);
                return settingsBtn;
            case "max_results":
                maxResultsAddedToLayout = true;
                return maxResultsLayout;
            case "fts_switch":
                return ftsSwitch;
            case "spacer":
                return controlsLayoutGap;
            case "pin":
            case "save":
            case "save_with_values":
            case "save_as":
            case "edit":
            case "remove":
            case "make_default":
            case "save_search_folder":
            case "save_app_folder":
                return createActionBtn(name, options);
            }
            return null;
        }

        protected Button createActionBtn(String actionName, List<String> options) {
            if (!isActionAllowed(actionName)) {
                return null;
            }
            Button button = componentsFactory.createComponent(Button.class);
            button.setAction(filterActions.get(actionName));
            if (options.contains("no-caption")) {
                button.setCaption(null);
                button.setDescription(filterActions.get(actionName).getCaption());
            }
            if (options.contains("no-icon")) {
                button.setIcon(null);
            }
            return button;
        }

        protected void fillSettingsBtn(List<String> actionNames) {
            for (String actionName : actionNames) {
                AbstractAction action = filterActions.get(actionName);
                if (action == null) {
                    log.warn("Action {} cannot be added to settingsBtn", actionName);
                    continue;
                }
                if (isActionAllowed(actionName)) {
                    settingsBtn.addAction(action);
                }
            }
        }

        protected boolean isActionAllowed(String actionName) {
            switch (actionName) {
            case "pin":
                return globalConfig.getAllowQueryFromSelected();
            case "save_search_folder":
            case "save_app_folder":
                return folderActionsEnabled && filterHelper.isFolderActionsEnabled();
            default:
                return true;
            }
        }
    }
}