ilscipio/scipio-erp

View on GitHub
framework/widget/src/org/ofbiz/widget/model/ModelMenu.java

Summary

Maintainability
F
3 days
Test Coverage
/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.ofbiz.widget.model;

import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.ofbiz.base.location.FlexibleLocation;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.base.util.UtilXml;
import org.ofbiz.base.util.collections.FlexibleMapAccessor;
import org.ofbiz.base.util.string.FlexibleStringExpander;
import org.ofbiz.widget.model.ModelMenuItem.MenuLink;
import org.ofbiz.widget.model.ModelMenuItem.ModelMenuItemAlias;
import org.ofbiz.widget.renderer.MenuRenderState;
import org.ofbiz.widget.renderer.MenuStringRenderer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;

/**
 * Models the <menu> element.
 *
 * @see <code>widget-menu.xsd</code>
 */
@SuppressWarnings("serial")
public class ModelMenu extends ModelMenuCommon implements ModelWidget.IdAttrWidget { // SCIPIO: new comon base class to share with sub-menu

    /*
     * ----------------------------------------------------------------------- *
     *                     DEVELOPERS PLEASE READ
     * ----------------------------------------------------------------------- *
     *
     * This model is intended to be a read-only data structure that represents
     * an XML element. Outside of object construction, the class should not
     * have any behaviors.
     *
     * Instances of this class will be shared by multiple threads - therefore
     * it is immutable. DO NOT CHANGE THE OBJECT'S STATE AT RUN TIME!
     *
     */

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    // SCIPIO: special/keyword menu and item names
    public static final String TOP_MENU_NAME = "TOP";
    static final Set<String> specialMenuNames = UtilMisc.toHashSet(TOP_MENU_NAME);

    private final List<ModelAction> actions;
    private final String defaultAlign;
    private final String defaultAlignStyle;
    private final FlexibleStringExpander defaultAssociatedContentId;
    private final String defaultCellWidth;
    private final String defaultDisabledTitleStyle;
    private final String defaultEntityName;
    private final Boolean defaultHideIfSelected;
    private final String defaultMenuItemName;
    private final String defaultPermissionEntityAction;
    private final String defaultPermissionOperation;
    private final String defaultSelectedStyle;
    private final String defaultSelectedAncestorStyle; // SCIPIO: new
    private final String defaultTitleStyle;
    private final String defaultTooltipStyle;
    private final String defaultWidgetStyle;
    private final String defaultLinkStyle; // SCIPIO
    private final FlexibleStringExpander extraIndex;
    private final String fillStyle;
    private final String id;
    private final FlexibleStringExpander menuContainerStyleExdr;
    /** This List will contain one copy of each item for each item name in the order
     * they were encountered in the service, entity, or menu definition; item definitions
     * with constraints will also be in this list but may appear multiple times for the same
     * item name.
     *
     * When rendering the menu the order in this list should be following and it should not be
     * necessary to use the Map. The Map is used when loading the menu definition to keep the
     * list clean and implement the override features for item definitions.
     */
    private final List<ModelMenuItem> menuItemList;
    /** This Map is keyed with the item name and has a ModelMenuItem for the value; items
     * with conditions will not be put in this Map so item definition overrides for items
     * with conditions is not possible.
     */
    private final Map<String, ModelMenuItem> menuItemMap;
    private final String menuLocation;
    private final String menuWidth;
    private final String orientation;
    private final ModelMenu parentMenu;
    /**
     * SCIPIO: List of selected menu item context field names (replaces single stock selectedMenuItemContextFieldName field).
     * <p>
     * We now support multiple lookups instead of only one.
     */
    private final List<FlexibleMapAccessor<String>> selectedMenuItemContextFieldName;
    //private final FlexibleMapAccessor<String> selectedMenuItemContextFieldName;
    private final FlexibleMapAccessor<String> selectedMenuContextFieldName; // SCIPIO: sub-menu locator
    private final String selectedMenuItemContextFieldNameStr;
    private final String target;
    private final FlexibleStringExpander title;
    private final String tooltip;
    private final String type;

    // SCIPIO: (other) new fields
    private final String itemsSortMode;

    private final Map<String, ModelSubMenu> subMenuMap; // SCIPIO: map of unique sub-menu names to sub-menus NOTE: only valid post-construction
    private final String autoSubMenuNames;
    private final String defaultSubMenuModelScope;
    private final String defaultSubMenuInstanceScope;

    private final String forceExtendsSubMenuModelScope;
    private final String forceAllSubMenuModelScope;

    private final boolean alwaysExpandSelectedOrAncestor;

    private final FlexibleStringExpander titleStyle;
    private final List<ModelMenuNode> manualSelectedNodes; // SCIPIO: cache of potentially manual selected items
    private final List<ModelMenuNode> manualExpandedNodes; // SCIPIO: cache of potentially manual expanded items

    private final Map<String, ModelMenuItemAlias> menuItemAliasMap;
    private final Map<String, String> menuItemNameAliasMap;

    // SCIPIO: 2017-04-25: new separated menu options
    private final FlexibleStringExpander separateMenuType;
    private final FlexibleStringExpander separateMenuTargetStyle;
    private final FlexibleStringExpander separateMenuTargetPreference;
    private final FlexibleStringExpander separateMenuTargetOriginalAction;
    private final FlexibleStringExpander itemConditionMode; // SCIPIO: 3.0.0: Added

    /** XML Constructor */
    public ModelMenu(Element menuElement, String menuLocation) {
        super(menuElement);
        // SCIPIO: This MUST be set early so the menu item constructor can get the location!
        this.menuLocation = menuLocation;
        GeneralBuildArgs genBuildArgs = new GeneralBuildArgs();

        ArrayList<ModelAction> actions = new ArrayList<>();
        String defaultAlign = "";
        String defaultAlignStyle = "";
        FlexibleStringExpander defaultAssociatedContentId = FlexibleStringExpander.getInstance("");
        String defaultCellWidth = "";
        String defaultDisabledTitleStyle = "";
        String defaultEntityName = "";
        Boolean defaultHideIfSelected = Boolean.FALSE;
        String defaultMenuItemName = "";
        String defaultPermissionEntityAction = "";
        String defaultPermissionOperation = "";
        String defaultSelectedStyle = "";
        String defaultSelectedAncestorStyle = ""; // SCIPIO
        String defaultTitleStyle = "";
        String defaultTooltipStyle = "";
        String defaultWidgetStyle = "";
        String defaultLinkStyle = ""; // SCIPIO
        FlexibleStringExpander extraIndex = FlexibleStringExpander.getInstance("");
        String fillStyle = "";
        String id = "";
        FlexibleStringExpander menuContainerStyleExdr = FlexibleStringExpander.getInstance("");
        ArrayList<ModelMenuItem> menuItemList = new ArrayList<>();
        Map<String, ModelMenuItem> menuItemMap = new HashMap<>();
        String menuWidth = "";
        String orientation = "horizontal";
        // SCIPIO: now using list
        //FlexibleMapAccessor<String> selectedMenuItemContextFieldName = FlexibleMapAccessor.getInstance("");
        List<FlexibleMapAccessor<String>> selectedMenuItemContextFieldName = new ArrayList<>();
        String selectedMenuItemContextFieldNameStr = "";
        FlexibleMapAccessor<String> selectedMenuContextFieldName = FlexibleMapAccessor.getInstance("");
        String target = "";
        FlexibleStringExpander title = FlexibleStringExpander.getInstance("");
        FlexibleStringExpander titleStyle = FlexibleStringExpander.getInstance(""); // SCIPIO
        String tooltip = "";
        String type = "";
        // SCIPIO: (other) new fields
        String itemsSortMode = "";
        String autoSubMenuNames = "";
        String defaultSubMenuModelScope = "";
        String defaultSubMenuInstanceScope = "";
        String forceExtendsSubMenuModelScope = "";
        String forceAllSubMenuModelScope = "";
        boolean alwaysExpandSelectedOrAncestor = false;
        FlexibleStringExpander separateMenuType = FlexibleStringExpander.getInstance("");
        FlexibleStringExpander separateMenuTargetStyle = FlexibleStringExpander.getInstance("");
        FlexibleStringExpander separateMenuTargetPreference = FlexibleStringExpander.getInstance("");
        FlexibleStringExpander separateMenuTargetOriginalAction = FlexibleStringExpander.getInstance("");
        FlexibleStringExpander itemConditionMode = FlexibleStringExpander.getInstance("");

        // check if there is a parent menu to inherit from
        ModelMenu parent = null;
        String parentResource = menuElement.getAttribute("extends-resource");
        String parentMenu = menuElement.getAttribute("extends");
        if (!parentMenu.isEmpty()) {
            parent = getMenuDefinition(parentResource, parentMenu, menuElement, genBuildArgs); // SCIPIO: this is now gotten from menuElement in safer way: menuLocation
            if (parent != null) {
                type = parent.type;
                itemsSortMode = parent.itemsSortMode;
                target = parent.target;
                id = parent.id;
                title = parent.title;
                titleStyle = parent.titleStyle;
                tooltip = parent.tooltip;
                defaultEntityName = parent.defaultEntityName;
                defaultTitleStyle = parent.defaultTitleStyle;
                defaultSelectedStyle = parent.defaultSelectedStyle;
                defaultSelectedAncestorStyle = parent.defaultSelectedAncestorStyle;
                defaultWidgetStyle = parent.defaultWidgetStyle;
                defaultLinkStyle = parent.defaultLinkStyle;
                defaultTooltipStyle = parent.defaultTooltipStyle;
                defaultMenuItemName = parent.defaultMenuItemName;

                defaultPermissionOperation = parent.defaultPermissionOperation;
                defaultPermissionEntityAction = parent.defaultPermissionEntityAction;
                defaultAssociatedContentId = parent.defaultAssociatedContentId;
                defaultHideIfSelected = parent.defaultHideIfSelected;
                orientation = parent.orientation;
                menuWidth = parent.menuWidth;
                defaultCellWidth = parent.defaultCellWidth;
                defaultDisabledTitleStyle = parent.defaultDisabledTitleStyle;
                defaultAlign = parent.defaultAlign;
                defaultAlignStyle = parent.defaultAlignStyle;
                fillStyle = parent.fillStyle;
                extraIndex = parent.extraIndex;
                autoSubMenuNames = parent.autoSubMenuNames;
                defaultSubMenuModelScope = parent.defaultSubMenuModelScope;
                defaultSubMenuInstanceScope = parent.defaultSubMenuInstanceScope;
                // SCIPIO: NOT inherited
                //forceExtendsSubMenuModelScope = parent.forceExtendsSubMenuModelScope;
                //forceAllSubMenuModelScope = parent.forceAllSubMenuModelScope;
                // SCIPIO: copy list
                //selectedMenuItemContextFieldName = parent.selectedMenuItemContextFieldName;
                selectedMenuItemContextFieldName = new ArrayList<FlexibleMapAccessor<String>>(parent.selectedMenuItemContextFieldName);
                selectedMenuItemContextFieldNameStr = parent.selectedMenuItemContextFieldNameStr;
                selectedMenuContextFieldName = parent.selectedMenuContextFieldName;
                menuContainerStyleExdr = parent.menuContainerStyleExdr;
                alwaysExpandSelectedOrAncestor = parent.alwaysExpandSelectedOrAncestor;

                separateMenuType = parent.separateMenuType;
                separateMenuTargetStyle = parent.separateMenuTargetStyle;
                separateMenuTargetPreference = parent.separateMenuTargetPreference;
                separateMenuTargetOriginalAction = parent.separateMenuTargetOriginalAction;
                itemConditionMode = parent.itemConditionMode;
            }
        }
        if (!menuElement.getAttribute("type").isEmpty()) {
            type = menuElement.getAttribute("type");
        }
        if (!menuElement.getAttribute("items-sort-mode").isEmpty()) {
            itemsSortMode = menuElement.getAttribute("items-sort-mode");
        }
        if (!menuElement.getAttribute("target").isEmpty()) {
            target = menuElement.getAttribute("target");
        }
        if (!menuElement.getAttribute("id").isEmpty()) {
            id = menuElement.getAttribute("id");
        }
        if (!menuElement.getAttribute("title").isEmpty()) {
            title = FlexibleStringExpander.getInstance(menuElement.getAttribute("title"));
        }
        if (!menuElement.getAttribute("title-style").isEmpty()) {
            titleStyle = FlexibleStringExpander.getInstance(menuElement.getAttribute("title-style"));
        }
        if (!menuElement.getAttribute("tooltip").isEmpty()) {
            tooltip = menuElement.getAttribute("tooltip");
        }
        if (!menuElement.getAttribute("default-entity-name").isEmpty()) {
            defaultEntityName = menuElement.getAttribute("default-entity-name");
        }
        // SCIPIO: MUST pass all the -style attributes through buildStyle to combine with parent values (modifies stock)
        if (!menuElement.getAttribute("default-title-style").isEmpty()) {
            defaultTitleStyle = buildStyle(menuElement.getAttribute("default-title-style"), parent != null ? parent.defaultTitleStyle : null, "");
        }
        if (!menuElement.getAttribute("default-selected-style").isEmpty()) {
            defaultSelectedStyle = buildStyle(menuElement.getAttribute("default-selected-style"), parent != null ? parent.defaultSelectedStyle : null, "");
        }
        if (!menuElement.getAttribute("default-selected-ancestor-style").isEmpty()) {
            defaultSelectedAncestorStyle = buildStyle(menuElement.getAttribute("default-selected-ancestor-style"), parent != null ? parent.defaultSelectedAncestorStyle : null, "");
        }
        if (!menuElement.getAttribute("default-widget-style").isEmpty()) {
            defaultWidgetStyle = buildStyle(menuElement.getAttribute("default-widget-style"), parent != null ? parent.defaultWidgetStyle : null, "");
        }
        if (!menuElement.getAttribute("default-link-style").isEmpty()) { // SCIPIO
            defaultLinkStyle = buildStyle(menuElement.getAttribute("default-link-style"), parent != null ? parent.defaultLinkStyle : null, "");
        }
        if (!menuElement.getAttribute("default-tooltip-style").isEmpty()) {
            defaultTooltipStyle = buildStyle(menuElement.getAttribute("default-tooltip-style"), parent != null ? parent.defaultTooltipStyle : null, "");
        }
        if (!menuElement.getAttribute("default-menu-item-name").isEmpty()) {
            defaultMenuItemName = menuElement.getAttribute("default-menu-item-name");
        }
        if (!menuElement.getAttribute("default-permission-operation").isEmpty()) {
            defaultPermissionOperation = menuElement.getAttribute("default-permission-operation");
        }
        if (!menuElement.getAttribute("default-permission-entity-action").isEmpty()) {
            defaultPermissionEntityAction = menuElement.getAttribute("default-permission-entity-action");
        }
        if (!menuElement.getAttribute("default-associated-content-id").isEmpty()) {
            defaultAssociatedContentId = FlexibleStringExpander.getInstance(menuElement
                    .getAttribute("default-associated-content-id"));
        }
        if (!menuElement.getAttribute("orientation").isEmpty()) {
            orientation = menuElement.getAttribute("orientation");
        }
        if (!menuElement.getAttribute("menu-width").isEmpty()) {
            menuWidth = menuElement.getAttribute("menu-width");
        }
        if (!menuElement.getAttribute("default-cell-width").isEmpty()) {
            defaultCellWidth = menuElement.getAttribute("default-cell-width");
        }
        if (!menuElement.getAttribute("default-hide-if-selected").isEmpty()) {
            defaultHideIfSelected = "true".equals(menuElement.getAttribute("default-hide-if-selected"));
        }
        if (!menuElement.getAttribute("default-disabled-title-style").isEmpty()) {
            defaultDisabledTitleStyle = buildStyle(menuElement.getAttribute("default-disabled-title-style"), parent != null ? parent.defaultDisabledTitleStyle : null, "");
        }
        if (!menuElement.getAttribute("selected-menuitem-context-field-name").isEmpty()) {
            selectedMenuItemContextFieldNameStr = menuElement.getAttribute("selected-menuitem-context-field-name");
            selectedMenuItemContextFieldName = makeAccessorList(selectedMenuItemContextFieldNameStr);
        }
        if (!menuElement.getAttribute("selected-menu-context-field-name").isEmpty()) { // SCIPIO
            String selectedMenuContextFieldNameStr = menuElement.getAttribute("selected-menu-context-field-name");
            selectedMenuContextFieldName = FlexibleMapAccessor.getInstance(selectedMenuContextFieldNameStr);
        }
        if (!menuElement.getAttribute("menu-container-style").isEmpty()) {
            menuContainerStyleExdr = FlexibleStringExpander.getInstance(buildStyle(menuElement.getAttribute("menu-container-style"), parent != null ? parent.menuContainerStyleExdr.getOriginal(): null, ""));
        }
        if (!menuElement.getAttribute("default-align").isEmpty()) {
            defaultAlign = menuElement.getAttribute("default-align");
        }
        if (!menuElement.getAttribute("default-align-style").isEmpty()) {
            defaultAlignStyle = buildStyle(menuElement.getAttribute("default-align-style"), parent != null ? parent.defaultAlignStyle : null, "");
        }
        if (!menuElement.getAttribute("fill-style").isEmpty()) {
            fillStyle = buildStyle(menuElement.getAttribute("fill-style"), parent != null ? parent.fillStyle : null, "");
        }
        if (!menuElement.getAttribute("extra-index").isEmpty()) {
            extraIndex = FlexibleStringExpander.getInstance(menuElement.getAttribute("extra-index"));
        }
        if (!menuElement.getAttribute("auto-sub-menu-names").isEmpty()) {
            autoSubMenuNames = menuElement.getAttribute("auto-sub-menu-names");
        }
        if (!menuElement.getAttribute("default-sub-menu-model-scope").isEmpty()) {
            defaultSubMenuModelScope = menuElement.getAttribute("default-sub-menu-model-scope");
        }
        if (!menuElement.getAttribute("default-sub-menu-include-scope").isEmpty()) {
            defaultSubMenuInstanceScope = menuElement.getAttribute("default-sub-menu-include-scope");
        }
        if (!menuElement.getAttribute("force-extends-sub-menu-model-scope").isEmpty()) {
            forceExtendsSubMenuModelScope = menuElement.getAttribute("force-extends-sub-menu-model-scope");
        }
        if (!menuElement.getAttribute("force-all-sub-menu-model-scope").isEmpty()) {
            forceAllSubMenuModelScope = menuElement.getAttribute("force-all-sub-menu-model-scope");
        }
        if (!menuElement.getAttribute("always-expand-selected-or-ancestor").isEmpty()) {
            alwaysExpandSelectedOrAncestor = "true".equals(menuElement.getAttribute("always-expand-selected-or-ancestor"));
        }
        separateMenuType = getExpander(menuElement, "separate-menu-type", separateMenuType);
        separateMenuTargetStyle = getExpander(menuElement, "separate-menu-target-style", separateMenuTargetStyle);
        separateMenuTargetPreference = getExpander(menuElement, "separate-menu-target-preference", separateMenuTargetPreference);
        if (separateMenuTargetPreference.isEmpty()) separateMenuTargetPreference = FlexibleStringExpander.getInstance("greatest-ancestor");
        separateMenuTargetOriginalAction = getExpander(menuElement, "separate-menu-target-original-action", separateMenuTargetOriginalAction);
        itemConditionMode = getExpander(menuElement, "item-condition-mode", itemConditionMode);

        this.autoSubMenuNames = autoSubMenuNames;
        this.defaultSubMenuModelScope = defaultSubMenuModelScope;
        this.defaultSubMenuInstanceScope = defaultSubMenuInstanceScope;
        this.forceExtendsSubMenuModelScope = forceExtendsSubMenuModelScope;
        this.forceAllSubMenuModelScope = forceAllSubMenuModelScope;

        this.defaultAlign = defaultAlign;
        this.defaultAlignStyle = defaultAlignStyle;
        this.defaultAssociatedContentId = defaultAssociatedContentId;
        this.defaultCellWidth = defaultCellWidth;
        this.defaultDisabledTitleStyle = defaultDisabledTitleStyle;
        this.defaultEntityName = defaultEntityName;
        this.defaultHideIfSelected = defaultHideIfSelected;
        this.defaultMenuItemName = defaultMenuItemName;
        this.defaultPermissionEntityAction = defaultPermissionEntityAction;
        this.defaultPermissionOperation = defaultPermissionOperation;
        this.defaultSelectedStyle = defaultSelectedStyle;
        this.defaultSelectedAncestorStyle = defaultSelectedAncestorStyle; // SCIPIO
        this.defaultTitleStyle = defaultTitleStyle;
        this.defaultTooltipStyle = defaultTooltipStyle;
        this.defaultWidgetStyle = defaultWidgetStyle;
        this.defaultLinkStyle = defaultLinkStyle; // SCIPIO
        this.extraIndex = extraIndex;
        this.fillStyle = fillStyle;
        this.id = id;
        this.menuContainerStyleExdr = menuContainerStyleExdr;

        this.menuWidth = menuWidth;
        this.orientation = orientation;
        this.parentMenu = parent;
        this.selectedMenuItemContextFieldName = Collections.unmodifiableList(selectedMenuItemContextFieldName); // SCIPIO
        this.selectedMenuItemContextFieldNameStr = selectedMenuItemContextFieldNameStr; // SCIPIO
        this.selectedMenuContextFieldName = selectedMenuContextFieldName; // SCIPIO
        this.target = target;
        this.title = title;
        this.titleStyle = titleStyle; // SCIPIO
        this.tooltip = tooltip;
        this.type = type;

        // SCIPIO: (other) new fields and new processing code
        this.itemsSortMode = itemsSortMode;
        this.alwaysExpandSelectedOrAncestor = alwaysExpandSelectedOrAncestor;

        this.separateMenuType = separateMenuType;
        this.separateMenuTargetStyle = separateMenuTargetStyle;
        this.separateMenuTargetPreference = separateMenuTargetPreference;
        this.separateMenuTargetOriginalAction = separateMenuTargetOriginalAction;

        this.itemConditionMode = itemConditionMode;

        CurrentMenuDefBuildArgs currentMenuDefBuildArgs = new CurrentMenuDefBuildArgs(this);
        Map<String, ModelMenuItemAlias> menuItemAliasMap = new HashMap<>();

        // SCIPIO: this is delayed so the cloning can read some fields from this model instance
        if (parent != null) {
            if (parent.actions != null) {
                actions.addAll(parent.actions);
            }
            if (parent.menuItemAliasMap != null) {
                menuItemAliasMap.putAll(parent.menuItemAliasMap);
            }

            // SCIPIO: we must CLONE the parent's items with updated backreferences
            //menuItemList.addAll(parent.menuItemList);
            //menuItemMap.putAll(parent.menuItemMap);
            String extendedForceSubMenuModelScope = null;
            if (UtilValidate.isNotEmpty(this.forceAllSubMenuModelScope)) {
                extendedForceSubMenuModelScope = this.forceAllSubMenuModelScope;
            } else if (UtilValidate.isNotEmpty(this.forceExtendsSubMenuModelScope)) {
                extendedForceSubMenuModelScope = this.forceExtendsSubMenuModelScope;
            }
            ModelMenuItem.BuildArgs itemBuildArgs = new ModelMenuItem.BuildArgs(genBuildArgs, currentMenuDefBuildArgs,
                    menuLocation, extendedForceSubMenuModelScope);

            ModelMenuItem.cloneModelMenuItems(parent.menuItemList,
                    menuItemList, menuItemMap, this, null, itemBuildArgs);
        }

        // SCIPIO: include-actions and actions
        processIncludeActions(menuElement, null, null, actions, menuLocation, true, currentMenuDefBuildArgs, genBuildArgs);

        actions.trimToSize();
        this.actions = Collections.unmodifiableList(actions);

        // SCIPIO: include-menu-items and menu-item
        processIncludeMenuItems(menuElement, null, null, menuItemList, menuItemMap, menuItemAliasMap, true,
                menuLocation, true, null, null, this.forceAllSubMenuModelScope, null,
                currentMenuDefBuildArgs, genBuildArgs);

        menuItemList.trimToSize();
        this.menuItemList = Collections.unmodifiableList(menuItemList);
        this.menuItemMap = Collections.unmodifiableMap(menuItemMap);
        // SCIPIO: This MUST be set early so the menu item constructor can get the location!
        //this.menuLocation = menuLocation;

        // SCIPIO: at the end, build the map/index of sub-menus
        Map<String, ModelSubMenu> subMenuMap = new HashMap<>();
        addAllSubMenus(subMenuMap, menuItemList);
        this.subMenuMap = Collections.unmodifiableMap(subMenuMap);

        this.menuItemAliasMap = Collections.unmodifiableMap(menuItemAliasMap);
        this.menuItemNameAliasMap = makeMenuItemNameAliasMap(menuItemAliasMap);

        // SCIPIO: cache refs to all the manually-flagged items so don't have to at runtime
        ArrayList<ModelMenuNode> manualSelectedItems = new ArrayList<>();
        ArrayList<ModelMenuNode> manualExpandedItems = new ArrayList<>();
        findManualStateFlaggedItems(menuItemList, manualSelectedItems, manualExpandedItems);
        manualSelectedItems.trimToSize();
        manualExpandedItems.trimToSize();
        this.manualSelectedNodes = Collections.unmodifiableList(manualSelectedItems);
        this.manualExpandedNodes = Collections.unmodifiableList(manualExpandedItems);
    }

    private static FlexibleStringExpander getExpander(Element element, String name, FlexibleStringExpander existing) { // SCIPIO: helper
        if (!element.getAttribute(name).isEmpty()) {
            return FlexibleStringExpander.getInstance(element.getAttribute(name));
        }
        return existing;
    }

    /**
     * SCIPIO: Menu loading factored out of main constructor and modified for reuse.
     * <p>
     * NOTE: 2016-09-30: the menuLocation argument was removed and replaced with more physical check
     * using the anyMenuElement (via WidgetDocumentInfo).
     */
    static ModelMenu getMenuDefinition(String resource, String name, Element anyMenuElement, GeneralBuildArgs genBuildArgs) {
        ModelMenu modelMenu = null;

        // SCIPIO: 2016-09-30: OVERRIDE this completely now
        String menuLocation = WidgetDocumentInfo.retrieveAlways(anyMenuElement).getResourceLocation();
        if (UtilValidate.isEmpty(menuLocation)) {
            // important to know when this fails now
            throw new IllegalStateException("Unable to get menu widget file original location. Error in code somewhere...");
        }

        final String fullLoc;
        if (resource != null && !resource.isEmpty()) {
            fullLoc = resource + "#" + name;
        } else {
            fullLoc = menuLocation + "#" + name;
        }

        // SCIPIO: added a local cache (on top of the external cache from getMenuFromLocation)
        // because we now very heavily reload local menus
        modelMenu = genBuildArgs.localModelMenuCache.get(fullLoc);
        if (modelMenu == null) {
            // SCIPIO: Added a superficial check for same-location to prevent some endless loops.
            // WARN: this is only a superficial check to prevent endless loops while refactoring menus.
            // it does not resolve to the actual file, but since almost everything is a component://
            // we're probably fine.
            if (resource != null && !resource.isEmpty() && !(menuLocation.equals(resource))) {
                try {
                    modelMenu = MenuFactory.getMenuFromLocation(resource, name);
                } catch (Exception e) {
                    Debug.logError(e, "Failed to load menu definition '" + name + "' at resource '" + resource
                            + "'", module);
                }
            } else {
                resource = menuLocation;

                // try to find a menu definition in the same file
                Element rootElement = anyMenuElement.getOwnerDocument().getDocumentElement();
                List<? extends Element> menuElements = UtilXml.childElementList(rootElement, "menu");
                for (Element menuElementEntry : menuElements) {
                    if (menuElementEntry.getAttribute("name").equals(name)) {
                        modelMenu = new ModelMenu(menuElementEntry, resource);
                        break;
                    }
                }
                if (modelMenu == null) {
                    Debug.logError("Failed to find menu definition '" + name + "' in same document.", module);
                } else {
                    genBuildArgs.localModelMenuCache.put(fullLoc, modelMenu);
                }
            }
            if (modelMenu != null) {
                genBuildArgs.localModelMenuCache.put(fullLoc, modelMenu);
            }
        }
        return modelMenu;
    }

    /**
     * SCIPIO: implements include-actions and actions reading (moved here).
     * Also does include-elements.
     * <p>
     * FIXME: this method interface and its arguments are a mess.
     */
    void processIncludeActions(Element parentElement, List<? extends Element> preInclElements, List<? extends Element> postInclElements, List<ModelAction> actions,
            String currResource, boolean processIncludes, CurrentMenuDefBuildArgs currentMenuDefBuildArgs, GeneralBuildArgs genBuildArgs) {
        // don't think any problems from local cache for actions
        final boolean useCache = true;
        final boolean cacheConsume = false;

        if (processIncludes) {
            List<Element> actionInclElements = new ArrayList<>();
            if (preInclElements != null) {
                actionInclElements.addAll(preInclElements);
            }
            actionInclElements.addAll(UtilXml.childElementList(parentElement, "include-elements"));
            actionInclElements.addAll(UtilXml.childElementList(parentElement, "include-actions"));
            if (postInclElements != null) {
                actionInclElements.addAll(postInclElements);
            }
            for (Element actionInclElement : getMergedIncludeDirectives(actionInclElements, currResource)) {
                String inclMenuName = actionInclElement.getAttribute("menu-name");
                String inclResource = actionInclElement.getAttribute("resource");
                String inclRecursive = actionInclElement.getAttribute("recursive");
                if (inclRecursive.isEmpty()) {
                    inclRecursive = "full";
                }
                String nextResource = UtilValidate.isNotEmpty(inclResource) ? inclResource : currResource;

                if ("no".equals(inclRecursive) || "includes-only".equals(inclRecursive) ||
                    "extends-only".equals(inclRecursive) || "full".equals(inclRecursive)) {
                    Element includedMenuElem = loadIncludedMenu(inclMenuName, inclResource,
                            parentElement, currResource, genBuildArgs.menuElemCache, useCache, cacheConsume);

                    // WARN: we're forced to load this menu model even though we were support to avoid it
                    // because we need some resolved attributes off it
                    ModelMenu includedMenuModel = getMenuDefinition(inclResource, inclMenuName, parentElement, genBuildArgs); // currResource
                    CurrentMenuDefBuildArgs includedNextCurrentMenuDefBuildArgs = new CurrentMenuDefBuildArgs(includedMenuModel != null ? includedMenuModel : this);

                    if (includedMenuElem != null) {
                        if ("extends-only".equals(inclRecursive) || "full".equals(inclRecursive)) {
                            String extendedResource = includedMenuElem.getAttribute("extends-resource");
                            String extendedMenuName = includedMenuElem.getAttribute("extends");
                            String extendedNextResource = UtilValidate.isNotEmpty(extendedResource) ? extendedResource : nextResource;

                            if (UtilValidate.isNotEmpty(extendedMenuName)) {
                                Element extendedMenuElem = loadIncludedMenu(extendedMenuName, extendedResource,
                                        includedMenuElem, nextResource, genBuildArgs.menuElemCache, useCache, cacheConsume);
                                if (extendedMenuElem != null) {

                                    ModelMenu extendedMenuModel = getMenuDefinition(extendedResource, extendedMenuName, includedMenuElem, genBuildArgs); // nextResource
                                    CurrentMenuDefBuildArgs extendedNextCurrentMenuDefBuildArgs = new CurrentMenuDefBuildArgs(extendedMenuModel != null ? extendedMenuModel : this);

                                    processIncludeActions(extendedMenuElem, null, null, actions,
                                            extendedNextResource, true,
                                            extendedNextCurrentMenuDefBuildArgs, genBuildArgs);
                                } else {
                                    Debug.logError("Failed to find (via include-actions or include-elements) parent menu definition '" +
                                            extendedMenuName + "' in resource '" + extendedNextResource + "'", module);
                                }
                            }
                        }

                        if ("includes-only".equals(inclRecursive) || "full".equals(inclRecursive)) {
                            processIncludeActions(includedMenuElem, null, null, actions,
                                    nextResource, true,
                                    includedNextCurrentMenuDefBuildArgs, genBuildArgs);
                        } else {
                            processIncludeActions(includedMenuElem, null, null, actions,
                                    nextResource, false,
                                    includedNextCurrentMenuDefBuildArgs, genBuildArgs);
                        }
                    } else {
                        Debug.logError("Failed to find include-actions or include-elements menu definition '" +
                                inclMenuName + "' in resource '" + nextResource + "'", module);
                    }
                } else {
                    Debug.logError("Unrecognized include-actions or include-elements recursive mode: " + inclRecursive, module);
                }
            }
        }

        // read all actions under the "actions" element
        Element actionsElement = UtilXml.firstChildElement(parentElement, "actions");
        if (actionsElement != null) {
            actions.addAll(ModelMenuAction.readSubActions(this, actionsElement));
        }
    }

    /**
     * SCIPIO: implements include-menu-items and menu-item reading (moved here).
     * Also does include-elements.
     * <p>
     * FIXME: this method interface and its arguments are a mess.
     */
    void processIncludeMenuItems(Element parentElement, List<? extends Element> preInclElements, List<? extends Element> postInclElements, List<ModelMenuItem> menuItemList,
            Map<String, ModelMenuItem> menuItemMap, Map<String, ModelMenuItemAlias> menuItemAliasMap, boolean includeMenuItemAliases, String currResource,
            boolean processIncludes, Set<String> excludeItems, String subMenusFilter, String forceSubMenuModelScope,
            ModelSubMenu parentSubMenu, CurrentMenuDefBuildArgs currentMenuDefBuildArgs, GeneralBuildArgs genBuildArgs) {
        // WARN: even local cache not fully used (cacheConsume=true so only uses cached from prev actions includes)
        // to be safe because known that menu-item Elements get written to in some places and
        // reuse _might_ affect results in complex includes (?).
        // final menus are cached anyway.
        final boolean useCache = true;
        final boolean cacheConsume = true;

        if (excludeItems == null) {
            excludeItems = new HashSet<>();
        }

        if (processIncludes) {
            List<Element> itemInclElements = new ArrayList<>();
            if (preInclElements != null) {
                itemInclElements.addAll(preInclElements);
            }
            itemInclElements.addAll(UtilXml.childElementList(parentElement, "include-elements"));
            itemInclElements.addAll(UtilXml.childElementList(parentElement, "include-menu-items"));
            if (postInclElements != null) {
                itemInclElements.addAll(postInclElements);
            }
            for (Element itemInclElement : getMergedIncludeDirectives(itemInclElements, currResource)) {
                String inclMenuName = itemInclElement.getAttribute("menu-name");
                String inclResource = itemInclElement.getAttribute("resource");
                String inclRecursive = itemInclElement.getAttribute("recursive");
                String inclForceSubMenuModelScope = itemInclElement.getAttribute("force-sub-menu-model-scope");
                if (inclRecursive.isEmpty()) {
                    inclRecursive = "full";
                }
                String inclSubMenus = itemInclElement.getAttribute("sub-menus");
                String nextSubMenusFilter;
                if ("none".equals(subMenusFilter) || "none".equals(inclSubMenus)) {
                    // "none" overrides all
                    nextSubMenusFilter = "none";
                } else {
                    nextSubMenusFilter = inclSubMenus;
                }
                boolean nextIncludeMenuItemAliases = UtilMisc.booleanValue(itemInclElement.getAttribute("include-menu-item-aliases"), true);

                // NOTE: this method implements the force-xxx-sub-menu-model-scope
                // propagation logic in general
                if (forceSubMenuModelScope == null || forceSubMenuModelScope.isEmpty()) {
                    forceSubMenuModelScope = inclForceSubMenuModelScope;
                }

                Set<String> inclExcludeItems = new HashSet<>();
                List<? extends Element> skipItemElems = UtilXml.childElementList(itemInclElement, "exclude-item");
                for (Element skipItemElem : skipItemElems) {
                    String itemName = skipItemElem.getAttribute("name");
                    if (UtilValidate.isNotEmpty(itemName)) {
                        inclExcludeItems.add(itemName);
                    }
                }

                String nextResource = UtilValidate.isNotEmpty(inclResource) ? inclResource : currResource;

                if ("no".equals(inclRecursive) || "includes-only".equals(inclRecursive) ||
                    "extends-only".equals(inclRecursive) || "full".equals(inclRecursive)) {
                    Element includedMenuElem = loadIncludedMenu(inclMenuName, inclResource,
                            parentElement, currResource, genBuildArgs.menuElemCache, useCache, cacheConsume);

                    if (includedMenuElem != null) {
                        inclExcludeItems.addAll(excludeItems);

                        // WARN: we're forced to load this menu model even though we were support to avoid it
                        // because we need some resolved attributes off it
                        // NOTE: this is not meant to be used for any backreferences; we want them all to point to 'this' menu
                        ModelMenu includedMenuModel = getMenuDefinition(inclResource, inclMenuName, parentElement, genBuildArgs); // currResource
                        CurrentMenuDefBuildArgs includedNextCurrentMenuDefBuildArgs = new CurrentMenuDefBuildArgs(includedMenuModel != null ? includedMenuModel : this);

                        String includedForceSubMenuModelScope = forceSubMenuModelScope;
                        if (UtilValidate.isEmpty(includedForceSubMenuModelScope)) {
                            includedForceSubMenuModelScope = includedMenuModel.forceAllSubMenuModelScope;
                        }

                        if ("extends-only".equals(inclRecursive) || "full".equals(inclRecursive)) {
                            String extendedResource = includedMenuElem.getAttribute("extends-resource");
                            String extendedMenuName = includedMenuElem.getAttribute("extends");
                            String extendedNextResource = UtilValidate.isNotEmpty(extendedResource) ? extendedResource : nextResource;
                            if (UtilValidate.isNotEmpty(extendedMenuName)) {

                                Element extendedMenuElem = loadIncludedMenu(extendedMenuName, extendedResource,
                                        includedMenuElem, nextResource, genBuildArgs.menuElemCache, useCache, cacheConsume);

                                ModelMenu extendedMenuModel = getMenuDefinition(extendedResource, extendedMenuName, includedMenuElem, genBuildArgs); // nextResource
                                CurrentMenuDefBuildArgs extendedNextCurrentMenuDefBuildArgs = new CurrentMenuDefBuildArgs(extendedMenuModel != null ? extendedMenuModel : this);

                                String extendedForceSubMenuModelScope = includedForceSubMenuModelScope;
                                if (UtilValidate.isEmpty(extendedForceSubMenuModelScope)) {
                                    extendedForceSubMenuModelScope = includedMenuModel.forceExtendsSubMenuModelScope;
                                    if (UtilValidate.isEmpty(extendedForceSubMenuModelScope)) {
                                        extendedForceSubMenuModelScope = extendedMenuModel.forceAllSubMenuModelScope;
                                    }
                                }

                                if (extendedMenuElem != null) {
                                    processIncludeMenuItems(extendedMenuElem, null, null, menuItemList, menuItemMap, menuItemAliasMap, nextIncludeMenuItemAliases,
                                            extendedNextResource, true, inclExcludeItems, nextSubMenusFilter, extendedForceSubMenuModelScope, parentSubMenu,
                                            extendedNextCurrentMenuDefBuildArgs, genBuildArgs);
                                } else {
                                    Debug.logError("Failed to find (via include-menu-items or include-elements) parent menu definition '" +
                                            extendedMenuName + "' in resource '" + extendedNextResource + "'", module);
                                }
                            }
                        }


                        if ("includes-only".equals(inclRecursive) || "full".equals(inclRecursive)) {
                            processIncludeMenuItems(includedMenuElem, null, null, menuItemList, menuItemMap, menuItemAliasMap, nextIncludeMenuItemAliases,
                                    nextResource, true, inclExcludeItems, nextSubMenusFilter, includedForceSubMenuModelScope, parentSubMenu,
                                    includedNextCurrentMenuDefBuildArgs, genBuildArgs);
                        } else {
                            processIncludeMenuItems(includedMenuElem, null, null, menuItemList, menuItemMap, menuItemAliasMap, nextIncludeMenuItemAliases,
                                    nextResource, false, inclExcludeItems, nextSubMenusFilter, includedForceSubMenuModelScope, parentSubMenu,
                                    includedNextCurrentMenuDefBuildArgs, genBuildArgs);
                        }
                    } else {
                        Debug.logError("Failed to find include-menu-items or include-elements menu definition '" + inclMenuName + "' in resource '" + nextResource + "'", module);
                    }
                } else {
                    Debug.logError("Unrecognized include-menu-items or include-elements recursive mode: " + inclRecursive, module);
                }
            }
        }

        // SCIPIO: NOTE: the first (non-recursive) call to this method actually sets omitSubMenus=false for itemBuildArgs.
        // that's why we can set overrideItemBuildArgs = itemBuildArgs.
        // addUpdateMenuItem is called with omitSubMenus=false, but there will be no submenus on existingMenuItem anyway
        // because the previous recursive calls already built existingMenuItem with omitSubMenus=true, and everything
        // else below with omitSubMenus=true as well. so it automagically works out.
        ModelMenuItem.BuildArgs itemBuildArgs = new ModelMenuItem.BuildArgs(genBuildArgs, currentMenuDefBuildArgs, currResource, forceSubMenuModelScope);
        itemBuildArgs.omitSubMenus = ("none".equals(subMenusFilter));
        ModelMenuItem.BuildArgs overrideItemBuildArgs = itemBuildArgs;

        if (includeMenuItemAliases) {
            List<? extends Element> aliasElements = UtilXml.childElementList(parentElement, "menu-item-alias");
            for(Element aliasElement : aliasElements) {
                // TODO: review: can't remember if this should be itemBuildArgs or something else... makes no difference yet
                ModelMenuItemAlias aliasModel = new ModelMenuItemAlias(aliasElement, this, parentSubMenu, itemBuildArgs);
                menuItemAliasMap.put(aliasModel.getName(), aliasModel);
            }
        }

        List<? extends Element> itemElements = UtilXml.childElementList(parentElement, "menu-item");
        for (Element itemElement : itemElements) {
            String itemName = itemElement.getAttribute("name");
            if (!excludeItems.contains(itemName)) {
                ModelMenuItem modelMenuItem;
                if (parentSubMenu != null) {
                    modelMenuItem = new ModelMenuItem(itemElement, parentSubMenu, overrideItemBuildArgs);
                } else {
                    modelMenuItem = new ModelMenuItem(itemElement, this, overrideItemBuildArgs);
                }
                addUpdateMenuItem(modelMenuItem, menuItemList, menuItemMap, itemBuildArgs);
            }
        }
    }

    String getAutoSubMenuNames(Element menuElement) {
        return menuElement.getAttribute("auto-sub-menu-names");
    }

    private Collection<Element> getMergedIncludeDirectives(Collection<Element> includeElems, String menuLocation) {
        if (includeElems.size() <= 0) {
            return includeElems;
        }

        // must preserve order
        Map<String, Element> dirMap = new LinkedHashMap<>();
        for(Element inclElem : includeElems) {
            String elemKey;

            String inclRef = inclElem.getAttribute("menu-ref");
            String inclMenuName = inclElem.getAttribute("menu-name");
            String inclResource = inclElem.getAttribute("resource");

            if (!inclRef.isEmpty()) {
                if ("sub-menu-model".equals(inclRef)) {
                    elemKey = "#sub-menu-model#";
                } else {
                    Debug.logError("Invalid ref value on include directive (" + menuLocation + "#" + getName() + ")", module);
                    continue;
                }
            } else {

                if (UtilValidate.isEmpty(inclResource) && !inclMenuName.startsWith("#")) {
                    inclResource = menuLocation;
                }
                elemKey = inclResource + "#" + inclMenuName;

                if (inclMenuName.isEmpty()) {
                    Debug.logError("Missing menu-name or ref attribute on include directive (" + menuLocation + "#" + getName() + ")", module);
                    continue;
                }
            }

            // here, we only want to keep the LAST include directive, so later ones override previous,
            // so must remove first
            if (dirMap.containsKey(elemKey)) {
                dirMap.remove(elemKey);
            }
            dirMap.put(elemKey, inclElem);
        }

        // 2016-08-25: SPECIAL CASE: if there's an entry with menu-name "sub-menu-model",
        // it's a reference to another entry. we must remove that entry, re-insert it where
        // we are and modify it.
        Element subMenuRefElem = dirMap.get("#sub-menu-model#");
        if (subMenuRefElem != null) {
            String subMenuModelKey = null;
            Element subMenuModelElem = null;
            for(Map.Entry<String, Element> entry : dirMap.entrySet()) {
                Element elem = entry.getValue();
                if ("true".equals(elem.getAttribute("is-sub-menu-model-entry"))) {
                    subMenuModelKey = entry.getKey();
                    subMenuModelElem = elem;
                }
            }
            if (subMenuModelElem == null) {
                Debug.logError("include directive has menu-ref 'sub-menu-model' to reference "
                        + "a parent element sub-menu-model, but no such model was defined on parent (" + menuLocation + "#" + getName() + ")",
                        module);
                dirMap.remove("#sub-menu-model#"); // can't substitute, so kill it
            } else {
                // remove the original entry
                dirMap.remove(subMenuModelKey);

                // Clone the ref entry so we can modify it
                Element newElem = (Element) subMenuRefElem.cloneNode(true);
                // transfer the attribs not overridden
                copyAllElemAttribsNotSet(subMenuModelElem, newElem, UtilMisc.toSet("menu-ref"));
                dirMap.put("#sub-menu-model#", newElem);
            }
        }

        return dirMap.values();
    }

    static void copyAllElemAttribsNotSet(Element srcElem, Element destElem, Collection<String> excludeAttribs) {
        NamedNodeMap attribs = srcElem.getAttributes();
        for(int i = 0; i < attribs.getLength(); i++) {
            String attrName = attribs.item(i).getNodeName();
            if (!excludeAttribs.contains(attrName)) {
                if (destElem.getAttribute(attrName).isEmpty() && !srcElem.getAttribute(attrName).isEmpty()) {
                    destElem.setAttribute(attrName, srcElem.getAttribute(attrName));
                }
            }
        }
    }


    // SCIPIO: made accessible
    Element loadIncludedMenu(String menuName, String resource,
            Element currMenuElem, String currResource,
            Map<String, Element> menuElemCache, boolean useCache, boolean cacheConsume) {
        Element inclMenuElem = null;
        Element inclRootElem = null;
        String targetResource;
        if (UtilValidate.isNotEmpty(resource)) {
            targetResource = resource;
        }
        else {
            targetResource = currResource;
        }

        String fullLocation = targetResource + "#" + menuName;
        if (useCache && menuElemCache.containsKey(fullLocation)) {
            inclMenuElem = menuElemCache.get(fullLocation);
            if (cacheConsume) {
                menuElemCache.remove(fullLocation);
            }
        }
        else {
            if (true) { // UtilValidate.isNotEmpty(resource)
                try {
                    URL menuFileUrl = FlexibleLocation.resolveLocation(targetResource);
                    Document menuFileDoc = UtilXml.readXmlDocument(menuFileUrl, true, true);
                    // SCIPIO: New: Save original location as user data in Document
                    if (menuFileDoc != null) {
                        WidgetDocumentInfo.retrieveAlways(menuFileDoc).setResourceLocation(targetResource);
                    }
                    inclRootElem = menuFileDoc.getDocumentElement();
                } catch (Exception e) {
                    Debug.logError(e, "Failed to load include-menu-items resource: " + resource, module);
                }
            }
            //else {
                // SCIPIO: No! we must reload the orig doc always because the Elements get written to!
                // must have fresh versions.
                // try to find a menu definition in the same file
                //inclRootElem = currMenuElem.getOwnerDocument().getDocumentElement();
            //}

            if (inclRootElem != null) {
                List<? extends Element> menuElements = UtilXml.childElementList(inclRootElem, "menu");
                for (Element menuElementEntry : menuElements) {
                    if (menuElementEntry.getAttribute("name").equals(menuName)) {
                        inclMenuElem = menuElementEntry;
                        break;
                    }
                }
            }
            if (useCache && !cacheConsume) {
                menuElemCache.put(fullLocation, inclMenuElem);
            }
        }
        return inclMenuElem;
    }

    @Override
    public void accept(ModelWidgetVisitor visitor) throws Exception {
        visitor.visit(this);
    }

    /**
     * add/override modelMenuItem using the menuItemList and menuItemMap
     * <p>
     * SCIPIO: made this static and accessible by ModelMenuItem.
     * <p>
     * NOTE: we assume the overriding modelMenuItem was initialized with the
     * proper backreferences already.
     */
    void addUpdateMenuItem(ModelMenuItem modelMenuItem, List<ModelMenuItem> menuItemList,
            Map<String, ModelMenuItem> menuItemMap, ModelMenuItem.BuildArgs buildArgs) {
        ModelMenuItem existingMenuItem = menuItemMap.get(modelMenuItem.getName());
        if (existingMenuItem != null) {
            // SCIPIO: support a replace mode as well
            ModelMenuItem mergedMenuItem;
            if ("replace".equals(modelMenuItem.getOverrideMode())) {
                mergedMenuItem = modelMenuItem;
                int existingItemIndex = menuItemList.indexOf(existingMenuItem);
                menuItemList.set(existingItemIndex, mergedMenuItem);
            } else if ("remove-replace".equals(modelMenuItem.getOverrideMode())) {
                menuItemList.remove(existingMenuItem);
                menuItemMap.remove(modelMenuItem.getName());
                mergedMenuItem = modelMenuItem;
                menuItemList.add(modelMenuItem);
            } else {
                // does exist, update the item by doing a merge/override
                mergedMenuItem = existingMenuItem.mergeOverrideModelMenuItem(modelMenuItem, buildArgs);
                int existingItemIndex = menuItemList.indexOf(existingMenuItem);
                menuItemList.set(existingItemIndex, mergedMenuItem);
            }
            menuItemMap.put(modelMenuItem.getName(), mergedMenuItem);
        } else {
            // does not exist, add to Map
            menuItemList.add(modelMenuItem);
            menuItemMap.put(modelMenuItem.getName(), modelMenuItem);
        }
    }

    // SCIPIO: find all sub-menus
    void addAllSubMenus(Map<String, ModelSubMenu> subMenuMap, List<ModelMenuItem> menuItemList) {
        for(ModelMenuItem menuItem : menuItemList) {
            menuItem.addAllSubMenus(subMenuMap);
        }
        if (subMenuMap.containsKey(this.getName())) {
            Debug.logError("Menu " + this.getName() + " contains a sub-menu with same name as the top-level menu; "
                    + "invalid and will be ignored in unique sub-menu lookups", module);
            subMenuMap.remove(this.getName());
        }
        if (subMenuMap.containsKey(ModelMenu.TOP_MENU_NAME)) {
            Debug.logError("Menu " + this.getName() + " contains a sub-menu having the special reserved value '" +
                    ModelMenu.TOP_MENU_NAME + "' as name; invalid and will be ignored in unique sub-menu lookups", module);
            subMenuMap.remove(TOP_MENU_NAME);
        }
        if (subMenuMap.containsKey(null)) {
            Debug.logError("Menu " + this.getName() + " contains a null key; should not happen", module);
            subMenuMap.remove(null);
        }
        if (subMenuMap.containsKey("")) {
            Debug.logError("Menu " + this.getName() + " contains an empty key; should not happen", module);
            subMenuMap.remove("");
        }
    }

    /**
     * SCIPIO: Cache refs to all the manually-flagged items so don't have to at runtime.
     */
    void findManualStateFlaggedItems(List<? extends ModelMenuNode> nodeList, List<ModelMenuNode> manualSelectedNodes, List<ModelMenuNode> manualExpandedNodes) {
        if (nodeList == null) {
            return;
        }
        for(ModelMenuNode node : nodeList) {
            // do in-depth first; lower levels last
            findManualStateFlaggedItems(node.getChildrenNodes(), manualSelectedNodes, manualExpandedNodes);
            // do us
            if (node.getSelected() != null && !node.getSelected().getOriginal().isEmpty()) {
                manualSelectedNodes.add(node);
            }
            if (node.getExpanded() != null && !node.getExpanded().getOriginal().isEmpty()) {
                manualExpandedNodes.add(node);
            }
        }
    }

    public List<ModelAction> getActions() {
        return actions;
    }

    @Override
    public String getBoundaryCommentName() {
        return menuLocation + "#" + getName();
    }

    public String getCurrentMenuName(Map<String, Object> context) {
        return getName();
    }

    public String getDefaultAlign() {
        return this.defaultAlign;
    }

    public String getDefaultAlignStyle() {
        return this.defaultAlignStyle;
    }

    public FlexibleStringExpander getDefaultAssociatedContentId() {
        return defaultAssociatedContentId;
    }

    public String getDefaultAssociatedContentId(Map<String, Object> context) {
        return defaultAssociatedContentId.expandString(context);
    }

    public String getDefaultCellWidth() {
        return this.defaultCellWidth;
    }

    public String getDefaultDisabledTitleStyle() {
        return this.defaultDisabledTitleStyle;
    }

    public String getDefaultEntityName() {
        return this.defaultEntityName;
    }

    public Boolean getDefaultHideIfSelected() {
        return this.defaultHideIfSelected;
    }

    public String getDefaultMenuItemName() {
        return this.defaultMenuItemName;
    }

    /**
     * SCIPIO: Returns default item name from submenu or from this menu if submenu is null,
     * or null if not set anywhere.
     */
    public String getDefaultMenuItemName(ModelSubMenu subMenu) {
        if (UtilValidate.isNotEmpty(subMenu.getDefaultMenuItemName())) {
            return subMenu.getDefaultMenuItemName();
        }
        return this.getDefaultMenuItemName();
    }

    public String getDefaultPermissionEntityAction() {
        return this.defaultPermissionEntityAction;
    }

    public String getDefaultPermissionOperation() {
        return this.defaultPermissionOperation;
    }

    public String getDefaultSelectedStyle() {
        return this.defaultSelectedStyle;
    }

    public String getDefaultSelectedAncestorStyle() { // SCIPIO
        return this.defaultSelectedAncestorStyle;
    }

    public String getDefaultTitleStyle() {
        return this.defaultTitleStyle;
    }

    public String getDefaultTooltipStyle() {
        return this.defaultTooltipStyle;
    }

    public String getDefaultWidgetStyle() {
        return this.defaultWidgetStyle;
    }

    public String getDefaultLinkStyle() { // SCIPIO
        return this.defaultLinkStyle;
    }

    public FlexibleStringExpander getExtraIndex() {
        return extraIndex;
    }

    public String getExtraIndex(Map<String, Object> context) {
        try {
            return extraIndex.expandString(context);
        } catch (Exception ex) {
            return "";
        }
    }

    public String getFillStyle() {
        return this.fillStyle;
    }

    public String getId() {
        return this.id;
    }

    public String getMenuContainerStyle(Map<String, Object> context) {
        return menuContainerStyleExdr.expandString(context);
    }

    public String getMenuContainerStyle() { // SCIPIO
        return menuContainerStyleExdr.getOriginal();
    }

    public FlexibleStringExpander getMenuContainerStyleExdr() {
        return menuContainerStyleExdr;
    }

    /**
     * SCIPIO: Builds a style string from current, parent, and default, based on "+"/"="
     * combination logic.
     * <p>
     * NOTE: subtle difference between null and empty string.
     * <p>
     * FIXME?: there is a loss of information here, because the +/= styles are always
     * removed in the final string. should preserve so macros can use?
     * <p>
     * FIXME: this is inefficient in cases where parent style does not need to be visited,
     * but can't change easily in java.
     */
    static String buildStyle(String style, String parentStyle, String defaultStyle) {
        String res;
        if (!style.isEmpty()) {
            // SCIPIO: support extending styles
            if (style.startsWith("+")) {
                String addStyles = style.substring(1);
                String inheritedStyles;
                if (parentStyle != null) {
                    inheritedStyles = parentStyle;
                } else {
                    inheritedStyles = defaultStyle;
                }
                if (inheritedStyles != null && !inheritedStyles.isEmpty()) {
                    if (!addStyles.isEmpty()) {
                        res = inheritedStyles + (addStyles.startsWith(" ") ? "" : " ") + addStyles;
                    } else {
                        res = inheritedStyles;
                    }
                } else {
                    // PRESERVE the original "+" so it may tricle down to FTL macros...
                    //res = addStyles;
                    res = style;
                }
            }
            else {
                // DON'T remove this anymore... let it trickle down to FTL macro for further interpretation
                //if (style.startsWith("=")) {
                //    style = style.substring(1);
                //}
                res = style;
            }
        } else if (parentStyle != null) {
            res = parentStyle;
        } else {
            res = defaultStyle;
        }
        if (res != null) {
            res = res.trim();
        }
        return res;
    }

    /**
     * Combines an extra style (like selected-style) to a main style
     * string (like widget-style).
     *
     * <p>NOTE: currently, the extra style is always added as an extra, and
     * never replaces. The extra's prefix (+/=) is stripped.</p>
     *
     * <p>ALWAYS USE THIS METHOD TO CONCATENATE EXTRA STYLES.</p>
     *
     * <p>SCIPIO: 3.0.0: Returns existing style if extraStyle empty.</p>
     * <p>SCIPIO: 1.x.x: Added.</p>
     */
    public static String combineExtraStyle(String style, String extraStyle) {
        String res;
        if (style == null) {
            style = "";
        } else {
            style = style.trim();
        }
        if (extraStyle == null) {
            extraStyle = "";
        } else {
            extraStyle = extraStyle.trim();
        }
        if (extraStyle.isEmpty() || "+".equals(extraStyle)) { // SCIPIO: 3.0.0: Added
            return style;
        }

        if (style.isEmpty()) {
            // In this case, prefix the result with "+" to be sure we don't
            // turn the string into a "replacing" string
            if (extraStyle.startsWith("=") || extraStyle.startsWith("+")) {
                res = "+" + extraStyle.substring(1);
            } else {
                res = "+" + extraStyle;
            }
        } else {
            // Here, resulting string prefix is left to the input style
            if (extraStyle.startsWith("=") || extraStyle.startsWith("+")) {
                res = style + " " + extraStyle.substring(1);
            } else {
                res = style + " " + extraStyle;
            }
        }

        return res;
    }

    public List<ModelMenuItem> getMenuItemList() {
        return menuItemList;
    }

    public List<ModelMenuItem> getOrderedMenuItemList(final Map<String, Object> context) {
        return getOrderedMenuItemList(context, getItemsSortMode(), getMenuItemList());
    }

    protected static List<ModelMenuItem> getOrderedMenuItemList(final Map<String, Object> context,
            String itemsSortMode, List<ModelMenuItem> menuItemList) {
        if (itemsSortMode != null && !"off".equals(itemsSortMode)) {
            boolean ignoreCase = itemsSortMode.endsWith("-ignorecase");
            if (ignoreCase) {
                itemsSortMode = itemsSortMode.substring(0, itemsSortMode.length() - "-ignorecase".length());
            }

            // remove non-sortables
            List<ModelMenuItem> sorted = new ArrayList<>(menuItemList.size());
            for(ModelMenuItem item : menuItemList) {
                if (!"off".equals(item.getSortMode())) {
                    sorted.add(item);
                }
            }

            Comparator<ModelMenuItem> cmp = null;
            if ("name".equals(itemsSortMode)) {
                if (ignoreCase) {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            return o1.getName().compareToIgnoreCase(o2.getName());
                        }
                    };
                }
                else {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            return o1.getName().compareTo(o2.getName());
                        }
                    };
                }
            } else if ("title".equals(itemsSortMode)) {
                if (ignoreCase) {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            return o1.getTitle(context).compareToIgnoreCase(o2.getTitle(context));
                        }
                    };
                } else {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            return o1.getTitle(context).compareTo(o2.getTitle(context));
                        }
                    };
                }
            } else if ("linktext".equals(itemsSortMode)) {
                if (ignoreCase) {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            MenuLink l1 = o1.getLink();
                            MenuLink l2 = o2.getLink();
                            if (l1 != null) {
                                if (l2 != null) {
                                    return l1.getTextExdr().expandString(context).compareToIgnoreCase(l2.getTextExdr().expandString(context));
                                }
                                else {
                                    return 1;
                                }
                            } else {
                                if (l2 != null) {
                                    return -1;
                                } else {
                                    return 0;
                                }
                            }
                        }
                    };
                } else {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            MenuLink l1 = o1.getLink();
                            MenuLink l2 = o2.getLink();
                            if (l1 != null) {
                                if (l2 != null) {
                                    return l1.getTextExdr().expandString(context).compareTo(l2.getTextExdr().expandString(context));
                                } else {
                                    return 1;
                                }
                            } else {
                                if (l2 != null) {
                                    return -1;
                                } else {
                                    return 0;
                                }
                            }
                        }
                    };
                }
            } else if ("displaytext".equals(itemsSortMode)) {
                if (ignoreCase) {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            return o1.getDisplayText(context).compareToIgnoreCase(o2.getDisplayText(context));
                        }
                    };
                } else {
                    cmp = new Comparator<ModelMenuItem>() {
                        @Override
                        public int compare(ModelMenuItem o1, ModelMenuItem o2) {
                            return o1.getDisplayText(context).compareTo(o2.getDisplayText(context));
                        }
                    };
                }
            }
            if (cmp != null) {
                Collections.sort(sorted, cmp);
            }

            // reintegrate with the items that weren't supposed to be sorted; preserve their positions
            // and insert the sorted ones around them
            List<ModelMenuItem> finalList = new ArrayList<>(menuItemList.size());
            Iterator<ModelMenuItem> sortedIt = sorted.iterator();
            for(ModelMenuItem origItem : menuItemList) {
                if ("off".equals(origItem.getSortMode())) {
                    finalList.add(origItem);
                } else {
                    finalList.add(sortedIt.next());
                }
            }

            return finalList;
        }
        return menuItemList;
    }

    public Map<String, ModelMenuItem> getMenuItemMap() {
        return menuItemMap;
    }

    public String getMenuLocation() {
        return menuLocation;
    }

    public String getMenuWidth() {
        return this.menuWidth;
    }

    /**
     * Gets menu item by name.
     * @deprecated SCIPIO: use {@link #getMenuItemByName}
     */
    @Deprecated
    public ModelMenuItem getModelMenuItemByName(String name) {
        return getMenuItemByName(name);
    }

    /**
     * Gets menu item by name.
     * <p>
     * SCIPIO: NOTE: does NOT translate menu items. you might have to call
     * {@link #getMappedMenuItemName} or {@link #getMenuItemByNameMapped}.
     */
    public ModelMenuItem getMenuItemByName(String name) {
        return this.menuItemMap.get(name);
    }

    public ModelMenuItem getMenuItemByNameMapped(String name) {
        return this.menuItemMap.get(getMappedMenuItemName(name));
    }

    /**
     * SCIPIO: Gets a menu item by name, with extended lookup support.
     * @deprecated INCOMPLETE, do not use until reviewed in future, misses mapping logic
     * <p>
     * SCIPIO: 2016-08-30: this method is enhanced so that it can lookup menu items
     * in sub-menus, using a dot syntax:
     * topMenuItemName.subMenuItemName.subSubMenuItemName
     * In case a menu item has multiple sub-menus, they can be disambiguated by adding an
     * intermediate sub-menu name prefixed with ":":
     * topMenuItemName.subMenu1:subMenuItemName.subSubMenu2:subSubMenuItemName
     */
    @Deprecated
    public MenuAndItemLookup resolveMenuAndItemByTrail(String nameExpr) {
        return resolveMenuAndItemByTrail(splitMenuItemTrailExpr(nameExpr));
    }

    public static String[] splitMenuItemTrailExpr(String nameExpr) {
        return nameExpr.split("\\.");
    }

    @Deprecated
    public MenuAndItemLookup resolveMenuAndItemByTrail(String[] nameList) {
        return resolveMenuAndItemByTrail(nameList[0], nameList, 1);
    }

    @Deprecated
    public MenuAndItemLookup resolveMenuAndItemByTrail(String name, String[] nameList, int nextNameIndex) {
        ModelMenuItem currLevelItem = this.menuItemMap.get(name);
        if (nextNameIndex >= nameList.length) {
            return new MenuAndItemLookup(currLevelItem);
        } else if (currLevelItem != null) {
            return currLevelItem.resolveMenuAndItemByTrail(nameList[nextNameIndex], nameList, nextNameIndex + 1);
        } else {
            return null;
        }
    }

    /**
     * SCIPIO: Finds nested menu item using format:
     * subMenuName:menuItemName.
     */
    public MenuAndItemLookup resolveMenuAndItem(String nameExpr) {
        String[] parts = nameExpr.split(":");
        if (parts.length >= 2) {
            return resolveMenuAndItem(parts[1], parts[0]);
        } else {
            return resolveMenuAndItem(parts[0], (String) null);
        }
    }

    public boolean isMenuNameTopMenu(String menuName) {
        return (menuName == null || menuName.isEmpty() || TOP_MENU_NAME.equals(menuName) || menuName.equals(getName()));
    }

    public boolean isMenuNameSubMenu(String menuName) {
        return getSubMenuByName(menuName) != null;
    }

    public boolean isMenuNameWithinMenu(String menuName) {
        return isMenuNameTopMenu(menuName) || isMenuNameSubMenu(menuName);
    }

    /**
     * Gets menu item for sub-menu (or its parent if PARENT applies), applying all the name translations as needed.
     */
    public MenuAndItemLookup resolveMenuAndItem(String menuItemName, String subMenuName, boolean mapNames) {
        if (isMenuNameTopMenu(subMenuName)) {
            return resolveMenuAndItem(menuItemName, subMenuName, (ModelSubMenu) null, mapNames);
        } else {
            ModelSubMenu subMenu = getSubMenuByName(subMenuName);
            return (subMenu != null) ? resolveMenuAndItem(menuItemName, subMenuName, subMenu, mapNames) : MenuAndItemLookup.EMPTY;
        }
    }
    public MenuAndItemLookup resolveMenuAndItem(String menuItemName, String subMenuName) {
        return resolveMenuAndItem(menuItemName, subMenuName, true);
    }

    public MenuAndItemLookup resolveMenuAndItem(String menuItemName, ModelSubMenu subMenu, boolean mapNames) {
        return resolveMenuAndItem(menuItemName, subMenu != null ? subMenu.getName() : null, subMenu, mapNames);
    }

    /**
     * SCIPIO: Gets menu item for sub-menu (or its parent if PARENT applies).
     * If subMenu is null, assumes the item is meant for lookup in the top menu (this method should only
     * be called after the sub menu name was validated).
     * <p>
     * NOTE: This handles the special PARENT/PARENT-WITHSUB/PARENT-NOSUB values, and NONE handling.
     * <p>
     * NOTE: this overload assumes subMenu was already looked up and merely stores subMenuName in the lookup result.
     */
    public MenuAndItemLookup resolveMenuAndItem(String menuItemName, String subMenuName, ModelSubMenu subMenu, boolean mapNames) {
        if (subMenu == null) { // top menu
            String mappedMenuItemName = mapNames ? this.getMappedMenuItemName(menuItemName) : menuItemName;

            ModelMenuItem menuItem = getMenuItemByName(mappedMenuItemName);
            if (menuItem == null && UtilValidate.isNotEmpty(getDefaultMenuItemName())) {
                mappedMenuItemName = getDefaultMenuItemName();
                menuItem = getMenuItemByName(mappedMenuItemName);
            }

            return new MenuAndItemLookup(null, menuItem, subMenuName, mappedMenuItemName, menuItemName, null);
        } else {
            String mappedMenuItemName = mapNames ? subMenu.getMappedMenuItemName(menuItemName) : menuItemName;
            if (ModelMenuItem.PARENT_MENU_ITEM_NAMES.contains(mappedMenuItemName)) {
                if (ModelMenuItem.PARENT_NOSUB.equals(mappedMenuItemName)) {
                    // here we ditch our sub-menu
                    return new MenuAndItemLookup(subMenu.getParentMenuItem().getParentSubMenu(), subMenu.getParentMenuItem(),
                            subMenuName, mappedMenuItemName, menuItemName, subMenu);
                } else {
                    return new MenuAndItemLookup(subMenu, subMenu.getParentMenuItem(), subMenuName, mappedMenuItemName, menuItemName, subMenu);
                }
            } else {
                ModelMenuItem menuItem = subMenu.getMenuItemByName(mappedMenuItemName);

                if (menuItem == null && UtilValidate.isNotEmpty(subMenu.getDefaultMenuItemName())) {
                    mappedMenuItemName = subMenu.getDefaultMenuItemName();
                    if (ModelMenuItem.PARENT_MENU_ITEM_NAMES.contains(mappedMenuItemName)) {
                        if (ModelMenuItem.PARENT_NOSUB.equals(mappedMenuItemName)) {
                            // here we ditch our sub-menu
                            return new MenuAndItemLookup(subMenu.getParentMenuItem().getParentSubMenu(), subMenu.getParentMenuItem(),
                                    subMenuName, mappedMenuItemName, menuItemName, subMenu);
                        } else {
                            return new MenuAndItemLookup(subMenu, subMenu.getParentMenuItem(), subMenuName, mappedMenuItemName,
                                    menuItemName, subMenu);
                        }
                    } else {
                        menuItem = subMenu.getMenuItemByName(mappedMenuItemName);
                    }
                }
                return new MenuAndItemLookup(subMenu, menuItem, subMenuName, mappedMenuItemName, menuItemName, subMenu);
            }
        }
    }

    public MenuAndItemLookup resolveMenuAndItem(String menuItemName, ModelSubMenu subMenu) {
        return resolveMenuAndItem(menuItemName, subMenu, true);
    }

    /**
     * SCIPIO: If menuItemName is NONE or equivalent and subMenu is non-null, returns the parent item of the sub-menu,
     * as default selection handling for null; else returns the passed defaultMenuItem.
     * <p>
     * NOTE: Performs no name translations on its own.
     * <code></code>
     */
    public ModelMenuItem getMenuItemIfNone(String menuItemName, ModelSubMenu subMenu, ModelMenuItem defaultMenuItem) {
        if (ModelMenuItem.isNoneMenuItemName(menuItemName) && subMenu != null) {
            return subMenu.getParentMenuItem();
        } else {
            return defaultMenuItem;
        }
    }

    /**
     * SCIPIO: get the sub-menu by unique name.
     * <p>
     * DEV NOTE: this method is only valid for use post-construction of ModelMenu.
     */
    public ModelSubMenu getSubMenuByName(String name) {
        // OPTIMIZATION: check not needed because "TOP", null and empty return null here
        //if (isMenuNameTopMenu(name)) {
        //    return null;
        //}
        return this.subMenuMap.get(name);
    }

    public String getOrientation() {
        return this.orientation;
    }

    public ModelMenu getParentMenu() {
        return parentMenu;
    }

    // SCIPIO: NOTE: all the stock getSelectedMenu* methods have been modified.
    // NOTE: 2018-09-04: The context-less methods have now been modified so they return the FlexibleMapAccessor<String>
    // instead of the original string - this could break compatibility in some rare cases, but virtually
    // no code except for internal Scipio code would have been using these overloads with no context,
    // unless somebody decided to copy-paste the PrepareComplexMenu.groovy script (unlikely).

    public String getSelectedMenuItemContextFieldNameExprStr() { // SCIPIO
        return selectedMenuItemContextFieldNameStr;
    }

    public FlexibleMapAccessor<String> getSelectedMenuItemContextFieldNameFirst() { // SCIPIO
        if (this.selectedMenuItemContextFieldName != null && !this.selectedMenuItemContextFieldName.isEmpty()) {
            return this.selectedMenuItemContextFieldName.get(0);
        }
        return null;
    }

    public List<FlexibleMapAccessor<String>> getSelectedMenuItemContextFieldNames() { // SCIPIO
        return this.selectedMenuItemContextFieldName;
    }

    /**
     * @deprecated SCIPIO: 2018-09-04: use {@link #getSelectedMenuItemContextFieldNames()}, which
     * now returns a list of FlexibleMapAccessor.
     */
    @Deprecated
    public List<FlexibleMapAccessor<String>> getSelectedMenuItemContextFieldNamesExpr() { // SCIPIO
        return this.selectedMenuItemContextFieldName;
    }

    /**
     * Returns selected menu item context field name.
     * @deprecated SCIPIO: because this now supports multiple values and extended syntax,
     *   must use {@link #getSelectedMenuItemContextFieldNameExprStr} or
     *   {@link #getSelectedMenuItemContextFieldNameFirst} to disambiguate.
     */
    @Deprecated
    public FlexibleMapAccessor<String> getSelectedMenuItemContextFieldName() {
        return getSelectedMenuItemContextFieldNameFirst();
    }

    /**
     * getSelectedMenuItemContextFieldName.
     * <p>
     * SCIPIO: WARN: This method has been modified; it is too limited so it no longer
     * handles the default-menu-item-name.
     * Use getSelectedMenuItem instead.
     */
    public String getSelectedMenuItemContextFieldName(Map<String, Object> context) {
        // SCIPIO: we support multiple lookups.
        //String menuItemName = this.selectedMenuItemContextFieldName.get(context);
        // SCIPIO: New code start...
        String menuItemName = null;
        String firstMenuItemName = null;
        for (FlexibleMapAccessor<String> fieldNameExpr : this.selectedMenuItemContextFieldName) {
            menuItemName = fieldNameExpr.get(context);
            // The menu item must not be empty AND it must exist in this menu to be accepted
            // But record firstMenuItemName to be able to handle defaultMenuItemName in legacy fashion.
            if (UtilValidate.isNotEmpty(menuItemName)) {
                if (firstMenuItemName == null) {
                    firstMenuItemName = menuItemName;
                }
                if (this.menuItemMap.containsKey(menuItemName)) {
                    break;
                }
                else {
                    menuItemName = null;
                }
            }
        }
        // firstMenuItemName is a hack to preserve legacy ofbiz behavior:
        // If we did find a non-empty entry but it didn't match any item in our menu,
        // don't fall back to defaultMenuItemName - just return the first non-matching item.
        // Only use the "is in menu" check for the fields fallback logic, but not for defaultMenuItemName.
        // defaultMenuItemName is only used if all fields in list were empty.
        if (UtilValidate.isEmpty(menuItemName) && UtilValidate.isNotEmpty(firstMenuItemName)) {
            menuItemName = firstMenuItemName;
        }
        // SCIPIO: ... new code end
        // SCIPIO: 2016-08-30: cannot do this here anymore
        //if (UtilValidate.isEmpty(menuItemName)) {
        //    return this.defaultMenuItemName;
        //}
        return menuItemName;
    }

    public String getSelectedMenuContextFieldName(Map<String, Object> context) {
        return this.selectedMenuContextFieldName.get(context);
    }

    public FlexibleMapAccessor<String> getSelectedMenuContextFieldName() {
        return this.selectedMenuContextFieldName;
    }

    /**
     * SCIPIO: Returns selected menu and item.
     */
    public MenuAndItemLookup getSelectedMenuAndItem(Map<String, Object> context, boolean logWarnings) {
        String fullSelItemName = getSelectedMenuItemContextFieldName(context);

        String selItemName;
        String selMenuName;
        if (UtilValidate.isNotEmpty(fullSelItemName)) {
            String[] parts = fullSelItemName.split(":");
            if (parts.length >= 2) {
                selItemName = parts[1];
                selMenuName = parts[0];
            } else {
                selItemName = parts[0];
                selMenuName = getSelectedMenuContextFieldName(context);
            }
        } else {
            selMenuName = getSelectedMenuContextFieldName(context);
            selItemName = null;
        }
        return getSelectedMenuAndItem(selItemName, selMenuName, logWarnings);
    }

    public MenuAndItemLookup getSelectedMenuAndItem(Map<String, Object> context) {
        return getSelectedMenuAndItem(context, true);
    }

    /**
     * Returns selected menu and item. The item will be either a child of
     * the (sub-)menu, the parent item of the sub-menu, or null, but the calling code should guard
     * against strange cases.
     *
     * <p>The (sub-)menu name supports special value "TOP". The item name supports
     * special values "NONE" (same as null except prevent default-menu-item-name fallback),
     * "PARENT-WITHSUB"/"PARENT", and "PARENT-NOSUB".</p>
     *
     * <p>Note the default menu item fallback (default-menu-item-name) is ONLY used if the queried selItemName
     * is empty, but NOT if it's the value NONE nor if the lookup fails. NONE bypasses it, while
     * failed lookups are handled by displaying the menu or sub-menu as selected but without any item selected,
     * as visual debugging help.</p>
     *
     * <p>SCIPIO: 1.x.x: Added.</p>
     */
    public MenuAndItemLookup getSelectedMenuAndItem(String selItemName, String selMenuName, boolean logWarnings) {
        boolean menuNameTopMenu = isMenuNameTopMenu(selMenuName);
        // perform sub-menu lookup
        ModelSubMenu subMenu = getSubMenuByName(selMenuName);
        if (subMenu != null || menuNameTopMenu) {
            // menu item lookup. performs all the mappings but NOT the fallbacks, which handle below.
            MenuAndItemLookup lookup = resolveMenuAndItem(selItemName, selMenuName, subMenu, true);
            if (lookup.hasMenuItem()) {
                return lookup;
            } else {
                // NOTE: here, NONE is not a failure.
                // TODO: REVIEW: we will not print warnings for now if the menu item name was simply empty
                // and it's the top level of the menu; otherwise there will simply be too many warnings all the time,
                // coming from non-complex menus like tab bars
                if (UtilValidate.isNotEmpty(selItemName) || subMenu != null) {
                    if (!ModelMenuItem.NONE.equals(lookup.getLookupMenuItemName()) && logWarnings) {
                        if (menuNameTopMenu) {
                            Debug.logWarning("Menu-item name [" + lookup.getLookupMenuItemName() + "] (mapped from [" + selItemName +
                                    "]) was not found under top level of complex menu [" + this.getFullLocationAndName() + "]", module);
                        } else {
                            Debug.logWarning("Menu-item name [" + lookup.getLookupMenuItemName() + "] (mapped from [" + selItemName +
                                    "]) was not found within sub-menu [" + selMenuName + "] under complex menu [" + this.getFullLocationAndName() + "]", module);
                        }
                    }
                }
                ModelMenuItem menuItem = null;
                if (subMenu != null) {
                    // 2016-09-28: EXTRA FALLBACK: here we will return the parent item of the sub-menu as target (same as "PARENT")
                    // this is the correct behavior for NONE and helps debug visually the other error cases.
                    menuItem = subMenu.getParentMenuItem();
                }
                return new MenuAndItemLookup(subMenu, menuItem);
            }
        } else {
            if (logWarnings) {
                Debug.logWarning("Sub-menu name [" + selMenuName + "] was not found under top complex menu [" + this.getFullLocationAndName() + "]", module);
            }
            return MenuAndItemLookup.EMPTY;
        }
    }

    public MenuAndItemLookup getSelectedMenuAndItem(String selItemName, String selMenuName) {
        return getSelectedMenuAndItem(selItemName, selMenuName, true);
    }

    @Override
    public Map<String, ModelMenuItemAlias> getMenuItemAliasMap() { // SCIPIO: new
        return menuItemAliasMap;
    }

    @Override
    public Map<String, String> getMenuItemNameAliasMap() { // SCIPIO: new
        return menuItemNameAliasMap;
    }

    public static List<String> readMenuItemNamesList(String namesStr) {
        if (UtilValidate.isEmpty(namesStr)) {
            return new ArrayList<>();
        }
        String[] namesArr = namesStr.split(",");
        ArrayList<String> namesList = new ArrayList<>(namesArr.length);
        for(String name : namesArr) {
            namesList.add(name.trim());
        }
        return namesList;
    }

    public static Set<String> readMenuItemNamesSet(String namesStr) {
        return new HashSet<>(readMenuItemNamesList(namesStr));
    }

    /**
     * Translates menu item for top menu.
     * NOTE: for top menu there shouldn't be any PARENT or PARENT-NOSUB entries, so we simply
     * return the original for those (which will result in a further warning in most, but that's better).
     * For null/empty/NONE, returns NONE.
     */
    public String getMappedMenuItemName(String menuItemName) {
        // translate null to NONE as first thing so it can be recognized in mappings (in theory; might not be used/usable or redundant)
        menuItemName = ModelMenuItem.getNoneMenuItemNameAsConstant(menuItemName);
        String res = this.menuItemNameAliasMap.get(menuItemName);
        if (UtilValidate.isNotEmpty(res)) {
            if (ModelMenuItem.PARENT_MENU_ITEM_NAMES.contains(res)) { // don't support PARENT-XX in top menu
                return menuItemName;
            } else {
                return res;
            }
        } else {
            return menuItemName;
        }
    }

    public List<String> getItemNamesAliasedTo(String forName) {
        List<String> names = new ArrayList<>();
        for (ModelMenuItemAlias itemAlias : menuItemAliasMap.values()) {
            if (Objects.equals(forName, itemAlias.getForName())) {
                names.add(itemAlias.getName());
            }
        }
        return names;
    }

    /**
     * Translates for sub-menu or, if null, this instance.
     */
    public String getMappedMenuItemName(String menuItemName, ModelSubMenu subMenu) {
        return subMenu != null ? subMenu.getMappedMenuItemName(menuItemName) : this.getMappedMenuItemName(menuItemName);
    }

    protected static Map<String, String> makeMenuItemNameAliasMap(Map<String, ModelMenuItemAlias> aliasMap) {
        Map<String, String> nameMap = new HashMap<>();
        for(ModelMenuItemAlias aliasModel : aliasMap.values()) {
            nameMap.put(aliasModel.getName(), aliasModel.getForName());
        }
        return nameMap;
    }

    public boolean isParentOf(ModelMenuItem menuItem) { // SCIPIO: new
        if (menuItem == null) {
            return false;
        }
        return menuItem.isSame(menuItemMap.get(menuItem.getName()));
    }

    public String getTarget() {
        return target;
    }

    public FlexibleStringExpander getTitle() {
        return title;
    }

    public String getTitle(Map<String, Object> context) {
        return title.expandString(context);
    }

    /**
     * SCIPIO: Special title style for some kinds of menus (has versatile/generic meaning).
     */
    public FlexibleStringExpander getTitleStyle() {
        return titleStyle;
    }

    /**
     * SCIPIO: Special title style for some kinds of menus (has versatile/generic meaning).
     */
    public String getTitleStyle(Map<String, Object> context) {
        return titleStyle.expandString(context);
    }

    public String getTooltip() {
        return this.tooltip;
    }

    public String getType() {
        return this.type;
    }

    public String getItemsSortMode() { // SCIPIO
        return this.itemsSortMode;
    }

    /**
     * Returns rendered menu item count.
     */
    public int renderedMenuItemCount(Map<String, Object> context) {
        int count = 0;
        for (ModelMenuItem item : this.menuItemList) {
            // SCIPIO: every item needs context prepare for it now, otherwise conditions and count may be wrong
            MenuRenderState renderState = MenuRenderState.retrieve(context);
            Object prevItemContext = item.prepareItemContext(context, renderState);

            if (item.shouldBeRendered(context)) {
                count++;
            }

            item.restoreItemContext(context, prevItemContext, renderState);
        }
        return count;
    }

    public String getAutoSubMenuNames() { // SCIPIO
        return autoSubMenuNames;
    }

    public String getDefaultSubMenuModelScope() { // SCIPIO
        return defaultSubMenuModelScope;
    }

    public String getDefaultSubMenuInstanceScope() { // SCIPIO
        return defaultSubMenuInstanceScope;
    }

    public String getForceExtendsSubMenuModelScope() { // SCIPIO
        return forceExtendsSubMenuModelScope;
    }

    public String getForceAllSubMenuModelScope() { // SCIPIO
        return forceAllSubMenuModelScope;
    }

    public boolean isAlwaysExpandSelectedOrAncestor() { // SCIPIO
        return this.alwaysExpandSelectedOrAncestor;
    }

    public List<ModelMenuNode> getManualSelectedNodes() { // SCIPIO
        return manualSelectedNodes;
    }

    public List<ModelMenuNode> getManualExpandedNodes() { // SCIPIO
        return manualExpandedNodes;
    }

    // SCIPIO: DEV NOTE: WARN: DO NOT CALL THESE separate menu accessors on the fly; use MenuRenderState instead
    private String getSeparateMenuType(Map<String, Object> context) { // SCIPIO
        return separateMenuType.expandString(context);
    }

    private String getSeparateMenuTargetStyle(Map<String, Object> context) { // SCIPIO
        return separateMenuTargetStyle.expandString(context);
    }

    private String getSeparateMenuTargetPreference(Map<String, Object> context) { // SCIPIO
        return separateMenuTargetPreference.expandString(context);
    }

    private String getSeparateMenuTargetOriginalAction(Map<String, Object> context) { // SCIPIO
        return separateMenuTargetOriginalAction.expandString(context);
    }

    public String getItemConditionMode(Map<String, Object> context) { // SCIPIO
        return itemConditionMode.expandString(context);
    }

    public SeparateMenuConfig getSeparateMenuConfig(Map<String, Object> context) { // SCIPIO
        return new SeparateMenuConfig(this, context);
    }

    public static class SeparateMenuConfig { // SCIPIO
        private final String type;
        private final String targetStyle;
        private final String targetPreference;
        private final String targetOriginalAction;
        private final boolean enabled;

        private SeparateMenuConfig(ModelMenu modelMenu, Map<String, Object> context) {
            this.type = modelMenu.getSeparateMenuType(context);
            this.targetStyle = modelMenu.getSeparateMenuTargetStyle(context);
            this.targetPreference = modelMenu.getSeparateMenuTargetPreference(context);
            this.targetOriginalAction = modelMenu.getSeparateMenuTargetOriginalAction(context);
            this.enabled = !type.isEmpty() && !targetStyle.isEmpty();
        }

        public String getType() {
            return type;
        }

        public String getTargetStyle() {
            return targetStyle;
        }

        public String getTargetPreference() {
            return targetPreference;
        }

        public String getTargetOriginalAction() {
            return targetOriginalAction;
        }

        public boolean isEnabled() {
            return enabled;
        }
    }

    /**
     * Renders this menu to a String, i.e. in a text format, as defined with the
     * MenuStringRenderer implementation.
     *
     * @param writer The Writer that the menu text will be written to
     * @param context Map containing the menu context; the following are
     *   reserved words in this context: parameters (Map), isError (Boolean),
     *   itemIndex (Integer, for lists only, otherwise null), menuName
     *   (String, optional alternate name for menu, defaults to the
     *   value of the name attribute)
     * @param menuStringRenderer An implementation of the MenuStringRenderer
     *   interface that is responsible for the actual text generation for
     *   different menu elements; implementing you own makes it possible to
     *   use the same menu definitions for many types of menu UIs
     */
    public void renderMenuString(Appendable writer, Map<String, Object> context, MenuStringRenderer menuStringRenderer)
            throws IOException {
        // SCIPIO: do this now so nothing can mess with the selection
        MenuRenderState renderState = MenuRenderState.retrieve(context);
        renderState.updateSelectedMenuAndItem(context);
        Object lastMenuInfo = renderState.getCurrentMenuInfo();
        try {
            renderState.updateCurrentMenu(this, context);
            AbstractModelAction.runSubActions(this.actions, context);
            if ("simple".equals(this.type)) {
                this.renderSimpleMenuString(writer, context, menuStringRenderer);
            } else {
                throw new IllegalArgumentException("The type " + this.getType() + " is not supported for menu with name "
                        + this.getName());
            }
        } finally {
            renderState.setCurrentMenuInfo(lastMenuInfo);
        }
    }

    public void renderSimpleMenuString(Appendable writer, Map<String, Object> context, MenuStringRenderer menuStringRenderer)
            throws IOException {
        // render menu open
        menuStringRenderer.renderMenuOpen(writer, context, this);

        // render formatting wrapper open
        menuStringRenderer.renderFormatSimpleWrapperOpen(writer, context, this);

        // render each menuItem row, except hidden & ignored rows
        for (ModelMenuItem item : this.getOrderedMenuItemList(context)) { // SCIPIO: switched this.menuItemList to getOrderedMenuItemList
            item.renderMenuItemString(writer, context, menuStringRenderer);
        }
        // render formatting wrapper close
        menuStringRenderer.renderFormatSimpleWrapperClose(writer, context, this);

        // render menu close
        menuStringRenderer.renderMenuClose(writer, context, this);
    }

    public void runActions(Map<String, Object> context) {
        AbstractModelAction.runSubActions(this.actions, context);
    }

    /**
     * SCIPIO: make list of flexible accessors from a semicolon-separated string.
     * TODO: support escaping semicolons
     */
    private static List<FlexibleMapAccessor<String>> makeAccessorList(String accessorsStr) {
        String[] parts = accessorsStr.split(";");
        List<FlexibleMapAccessor<String>> list = new ArrayList<>(parts.length);
        for(String part : parts) {
            list.add(FlexibleMapAccessor.<String>getInstance(part));
        }
        return list;
    }


    /**
     * SCIPIO: Passed across the whole top menu render including all its included externals.
     */
    public static class GeneralBuildArgs {
        /**
         * WARN: this is not an exact figure and is mostly for generating names.
         */
        public int totalSubMenuCount = 0;
        /**
         * WARN: this is not an exact figure and is mostly for generating names.
         */
        public int totalMenuItemCount = 0;
        public Map<String, ModelMenu> localModelMenuCache = new HashMap<>();

        public final Map<String, Element> menuElemCache = new HashMap<>();
    }

    /**
     * SCIPIO: passed across the render of only those elements whose XML falls within
     * the current top-level menu.
     */
    public static class CurrentMenuDefBuildArgs {
        public MenuDefCodeBehavior codeBehavior;

        public CurrentMenuDefBuildArgs(ModelMenu modelMenu) {
            this.codeBehavior = new MenuDefCodeBehavior(modelMenu);
        }

        public CurrentMenuDefBuildArgs(MenuDefCodeBehavior codeBehavior) {
            this.codeBehavior = codeBehavior;
        }
    }

    public static class MenuDefCodeBehavior implements Serializable {
        public String autoSubMenuNames;
        public String defaultSubMenuModelScope;
        public String defaultSubMenuInstanceScope;

        public MenuDefCodeBehavior() {
            this.autoSubMenuNames = null;
            this.defaultSubMenuModelScope = null;
            this.defaultSubMenuInstanceScope = null;
        }

        public MenuDefCodeBehavior(ModelMenu modelMenu) {
            this.autoSubMenuNames = modelMenu.autoSubMenuNames;
            this.defaultSubMenuModelScope = modelMenu.defaultSubMenuModelScope;
            this.defaultSubMenuInstanceScope = modelMenu.defaultSubMenuInstanceScope;
        }
    }

    /**
     * SCIPIO: Simple sub-menu and menu-item pair. One or both may be null.
     * Usually when subMenu is null (while menuItem is non-null) it means the item is top level.
     */
    public static class MenuAndItem implements Serializable {
        public static final MenuAndItemLookup EMPTY = new MenuAndItemLookup();

        protected final ModelSubMenu subMenu;
        protected final ModelMenuItem menuItem;

        public MenuAndItem(ModelSubMenu subMenu, ModelMenuItem menuItem) {
            this.subMenu = subMenu;
            this.menuItem = menuItem;
        }

        /**
         * Makes pair from the passed menuItem and its parent sub-menu (if any).
         */
        public MenuAndItem(ModelMenuItem menuItem) {
            this.subMenu = (menuItem != null) ? menuItem.getParentSubMenu() : null;
            this.menuItem = menuItem;
        }

        public MenuAndItem() {
            this.subMenu = null;
            this.menuItem = null;
        }

        public boolean isEmpty() {
            return this.subMenu == null && this.menuItem == null;
        }

        public static boolean isEmpty(MenuAndItem menuAndItem) {
            return menuAndItem == null || menuAndItem.isEmpty();
        }

        public ModelSubMenu getSubMenu() {
            return subMenu;
        }

        public ModelMenuItem getMenuItem() {
            return menuItem;
        }

        public boolean isItemTopLevel() {
            return menuItem != null && subMenu == null;
        }

        public boolean isSubMenu() {
            return subMenu != null;
        }

        public boolean hasMenuItem() {
            return menuItem != null;
        }
    }

    /**
     * (Sub)Menu and item lookup result, including the lookup names used to query them.
     * The lookup names may differ from the effective names of the instances.
     */
    public static class MenuAndItemLookup extends MenuAndItem {
        public static final MenuAndItemLookup EMPTY = new MenuAndItemLookup();

        protected final String lookupSubMenuName;
        protected final String lookupMenuItemName;
        protected final String origLookupMenuItemName; // may be unmapped alias name
        protected final ModelSubMenu origLookupSubMenu;

        public MenuAndItemLookup(ModelSubMenu subMenu, ModelMenuItem menuItem,
                                 String lookupSubMenuName, String lookupMenuItemName,
                                 String origLookupMenuItemName, ModelSubMenu origLookupSubMenu) {
            super(subMenu, menuItem);
            this.lookupSubMenuName = lookupSubMenuName;
            this.lookupMenuItemName = lookupMenuItemName;
            this.origLookupMenuItemName = origLookupMenuItemName;
            this.origLookupSubMenu = origLookupSubMenu;
        }

        public MenuAndItemLookup(ModelSubMenu subMenu, ModelMenuItem menuItem) {
            super(subMenu, menuItem);
            this.lookupSubMenuName = null;
            this.lookupMenuItemName = null;
            this.origLookupMenuItemName = null;
            this.origLookupSubMenu = null;
        }

        public MenuAndItemLookup(ModelMenuItem menuItem) {
            super(menuItem);
            this.lookupSubMenuName = null;
            this.lookupMenuItemName = null;
            this.origLookupMenuItemName = null;
            this.origLookupSubMenu = null;
        }

        public MenuAndItemLookup() {
            this.lookupSubMenuName = null;
            this.lookupMenuItemName = null;
            this.origLookupMenuItemName = null;
            this.origLookupSubMenu = null;
        }

        public MenuAndItem toMenuAndItem() {
            return new MenuAndItem(this.subMenu, this.menuItem);
        }

        public String getLookupSubMenuName() {
            return lookupSubMenuName;
        }

        public String getLookupMenuItemName() {
            return lookupMenuItemName;
        }

        public String getOrigLookupMenuItemName() {
            return origLookupMenuItemName;
        }

        public ModelSubMenu getOrigLookupSubMenu() {
            return origLookupSubMenu;
        }
    }

    // SCIPIO: ModelMenuNode methods (new)

    @Override
    public ModelMenuItemNode getParentNode() {
        return null;
    }

    @Override
    public List<ModelMenuItem> getChildrenNodes() {
        return menuItemList;
    }

    @Override
    public FlexibleStringExpander getSelected() {
        return null;
    }

    @Override
    public FlexibleStringExpander getDisabled() {
        return null;
    }

    @Override
    public FlexibleStringExpander getExpanded() {
        return null;
    }

    public static class FlaggedMenuNodes {
        private final Set<ModelMenuNode> selectedTargets;
        private final Set<ModelMenuNode> selectedAncestors;
        private final Set<ModelMenuNode> expanded;

        public FlaggedMenuNodes(Set<ModelMenuNode> selectedTargets, Set<ModelMenuNode> selectedAncestors,
                Set<ModelMenuNode> expanded) {
            super();
            this.selectedTargets = selectedTargets;
            this.selectedAncestors = selectedAncestors;
            this.expanded = expanded;
        }

        public static FlaggedMenuNodes resolve(Map<String, Object> context, List<ModelMenuNode> selectedNodeCandidates,
                List<ModelMenuNode> expandedNodeCandidates, MenuAndItem mainSelectedMenuAndItem) {
            Set<ModelMenuNode> selectedTargets = new HashSet<>();
            Set<ModelMenuNode> selectedAncestors = new HashSet<>();
            Set<ModelMenuNode> expanded = new HashSet<>();

            // note: mainSelectedMenuAndItem counts as both selected(On) AND expanded(On)

            // SELECTED
            Set<ModelMenuNode> selectedOn = new HashSet<>();
            Set<ModelMenuNode> selectedOff = new HashSet<>();
            for(ModelMenuNode node : selectedNodeCandidates) {
                Boolean selectedBool = UtilMisc.booleanValue(node.getSelected().expandString(context));
                if (Boolean.TRUE.equals(selectedBool)) {
                    selectedOn.add(node);
                } else if (Boolean.TRUE.equals(selectedBool)) {
                    selectedOff.add(node);
                }
            }
            if (mainSelectedMenuAndItem.getSubMenu() != null) {
                selectedOn.add(mainSelectedMenuAndItem.getSubMenu());
            }
            if (mainSelectedMenuAndItem.getMenuItem() != null) {
                selectedOn.add(mainSelectedMenuAndItem.getMenuItem());
            }

            // TODO

            // EXPANDED
            Set<ModelMenuNode> expandedOn = new HashSet<>();
            Set<ModelMenuNode> expandedOff = new HashSet<>();
            for(ModelMenuNode node : expandedNodeCandidates) {
                Boolean expandedBool = UtilMisc.booleanValue(node.getExpanded().expandString(context));
                if (Boolean.TRUE.equals(expandedBool)) {
                    expandedOn.add(node);
                } else if (Boolean.TRUE.equals(expandedBool)) {
                    expandedOff.add(node);
                }
            }
            if (mainSelectedMenuAndItem.getSubMenu() != null) {
                expandedOn.add(mainSelectedMenuAndItem.getSubMenu());
            }
            if (mainSelectedMenuAndItem.getMenuItem() != null) {
                expandedOn.add(mainSelectedMenuAndItem.getMenuItem());
            }

            // TODO

            return new FlaggedMenuNodes(selectedTargets, selectedAncestors, expanded);
        }

        public boolean isSelectedTarget(ModelMenuNode node) {
            return selectedTargets.contains(node);
        }

        public boolean isSelectedAncestor(ModelMenuNode node) {
            return selectedAncestors.contains(node);
        }

        public boolean isExpanded(ModelMenuNode node) {
            return expanded.contains(node);
        }

        public Set<ModelMenuNode> getSelectedTargets() {
            return selectedTargets;
        }

        public Set<ModelMenuNode> getSelectedAncestors() {
            return selectedAncestors;
        }

        public Set<ModelMenuNode> getExpanded() {
            return expanded;
        }
    }

    @Override
    public String getContainerLocation() { // SCIPIO: new
        return menuLocation;
    }

    @Override
    public String getWidgetType() { // SCIPIO: new
        return "menu";
    }

}