protected boolean validateAndNormalize()

in core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java [238:523]


    protected boolean validateAndNormalize(
            final Grid grid,
            final Class<?> domainClass) {

        var bsGrid = (BSGrid) grid;
        var objectSpec = specificationLoader.specForTypeElseFail(domainClass);

        var oneToOneAssociationById = ObjectMember.mapById(objectSpec.streamProperties(MixedIn.INCLUDED));
        var oneToManyAssociationById = ObjectMember.mapById(objectSpec.streamCollections(MixedIn.INCLUDED));
        var objectActionById = ObjectMember.mapById(objectSpec.streamRuntimeActions(MixedIn.INCLUDED));

        var propertyLayoutDataById = bsGrid.getAllPropertiesById();
        var collectionLayoutDataById = bsGrid.getAllCollectionsById();
        var actionLayoutDataById = bsGrid.getAllActionsById();

        var gridModelIfValid = _GridModel.createFrom(bsGrid);
        if(!gridModelIfValid.isPresent()) { // only present if valid
            return false;
        }
        var gridModel = gridModelIfValid.get();

        var layoutDataFactory = LayoutDataFactory.of(objectSpec);

        // * surplus ... those defined in the grid model but not available with the meta-model
        // * missing ... those available with the meta-model but missing in the grid-model
        // (missing properties will be added to the first field-set of the specified column)

        var surplusAndMissingPropertyIds =
                surplusAndMissing(propertyLayoutDataById.keySet(),  oneToOneAssociationById.keySet());
        var surplusPropertyIds = surplusAndMissingPropertyIds.surplus();
        var missingPropertyIds = surplusAndMissingPropertyIds.missing();

        for (String surplusPropertyId : surplusPropertyIds) {
            propertyLayoutDataById.get(surplusPropertyId).setMetadataError("No such property");
        }

        // catalog which associations are bound to an existing field-set
        // so that (below) we can determine which missing property IDs are not unbound vs
        // which should be included in the field-set that they are bound to.
        var boundAssociationIdsByFieldSetId = _Maps.<String, Set<String>>newHashMap();

        for (var fieldSet : gridModel.fieldSets()) {
            var fieldSetId = GroupIdAndName.forFieldSet(fieldSet)
                .orElseThrow(()->_Exceptions.illegalArgument("invalid fieldSet detected, "
                        + "requires at least an id or a name"))
                .id();
            Set<String> boundAssociationIds = boundAssociationIdsByFieldSetId.get(fieldSetId);
            if(boundAssociationIds == null) {
                boundAssociationIds = stream(fieldSet.getProperties())
                        .map(PropertyLayoutData::getId)
                        .collect(Collectors.toCollection(_Sets::newLinkedHashSet));
                boundAssociationIdsByFieldSetId.put(fieldSetId, boundAssociationIds);
            }
        }

        // 1-to-1-association IDs, that want to contribute to the 'metadata' FieldSet
        // but are unbound, because such a FieldSet is not defined by the given layout.
        var unboundMetadataContributingIds = _Sets.<String>newHashSet();

        // along with any specified by existing metadata
        for (final OneToOneAssociation oneToOneAssociation : oneToOneAssociationById.values()) {
            var layoutGroupFacet = oneToOneAssociation.getFacet(LayoutGroupFacet.class);
            if(layoutGroupFacet == null) {
                continue;
            }
            var id = layoutGroupFacet.getGroupId();
            if(gridModel.containsFieldSetId(id)) {
                Set<String> boundAssociationIds =
                        boundAssociationIdsByFieldSetId.computeIfAbsent(id, k -> _Sets.newLinkedHashSet());
                boundAssociationIds.add(oneToOneAssociation.getId());
            } else if(id.equals(LayoutConstants.FieldSetId.METADATA)) {
                unboundMetadataContributingIds.add(oneToOneAssociation.getId());
            }
        }

        if(!missingPropertyIds.isEmpty()) {

            var unboundPropertyIds = _Sets.newLinkedHashSet(missingPropertyIds);

            for (final String fieldSetId : boundAssociationIdsByFieldSetId.keySet()) {
                var boundPropertyIds = boundAssociationIdsByFieldSetId.get(fieldSetId);
                unboundPropertyIds.removeAll(boundPropertyIds);
            }

            for (final String fieldSetId : boundAssociationIdsByFieldSetId.keySet()) {
                var fieldSet = gridModel.getFieldSet(fieldSetId);
                var associationIds = boundAssociationIdsByFieldSetId.get(fieldSetId);

                var associations1To1Ids =
                        associationIds.stream()
                        .map(oneToOneAssociationById::get)
                        .filter(_NullSafe::isPresent)
                        .sorted(ObjectMember.byMemberOrderSequence(false))
                        .map(ObjectAssociation::getId)
                        .collect(Collectors.toList());

                addPropertiesTo(
                        fieldSet,
                        associations1To1Ids,
                        layoutDataFactory::createPropertyLayoutData,
                        propertyLayoutDataById::put);
            }

            if(!unboundPropertyIds.isEmpty()) {
                var fieldSet = gridModel.getFieldSetForUnreferencedPropertiesRef();
                if(fieldSet != null) {
                    unboundPropertyIds.removeAll(unboundMetadataContributingIds);

                    // add unbound properties respecting configured sequence policy
                    var sortedUnboundPropertyIds = _UnreferencedSequenceUtil
                            .sortProperties(config, unboundPropertyIds.stream()
                                    .map(oneToOneAssociationById::get)
                                    .filter(_NullSafe::isPresent));

                    addPropertiesTo(
                            fieldSet,
                            sortedUnboundPropertyIds,
                            layoutDataFactory::createPropertyLayoutData,
                            propertyLayoutDataById::put);
                }
            }
        }

        // any missing collections will be added as tabs to a new TabGroup in the specified column
        var surplusAndMissingCollectionIds =
                surplusAndMissing(collectionLayoutDataById.keySet(), oneToManyAssociationById.keySet());
        var surplusCollectionIds = surplusAndMissingCollectionIds.surplus();
        var missingCollectionIds = surplusAndMissingCollectionIds.missing();

        for (String surplusCollectionId : surplusCollectionIds) {
            collectionLayoutDataById.get(surplusCollectionId).setMetadataError("No such collection");
        }

        if(!missingCollectionIds.isEmpty()) {

            // add missing collections respecting configured sequence policy
            var sortedMissingCollectionIds = _UnreferencedSequenceUtil
                    .sortCollections(config, missingCollectionIds.stream()
                            .map(oneToManyAssociationById::get)
                            .filter(_NullSafe::isPresent));

            final BSTabGroup bsTabGroup = gridModel.getTabGroupForUnreferencedCollectionsRef();
            if(bsTabGroup != null) {
                addCollectionsTo(
                        bsTabGroup,
                        sortedMissingCollectionIds,
                        objectSpec,
                        layoutDataFactory::createCollectionLayoutData);
            } else {
                final BSCol bsCol = gridModel.getColForUnreferencedCollectionsRef();
                if(bsCol != null) {
                    addCollectionsTo(
                        bsCol,
                        sortedMissingCollectionIds,
                        layoutDataFactory::createCollectionLayoutData,
                        collectionLayoutDataById::put);
                }
            }
        }

        // any missing actions will be added as actions in the specified column
        var surplusAndMissingActionIds =
                surplusAndMissing(actionLayoutDataById.keySet(), objectActionById.keySet());
        var surplusActionIds = surplusAndMissingActionIds.surplus();
        var possiblyMissingActionIds = surplusAndMissingActionIds.missing();

        final List<String> associatedActionIds = _Lists.newArrayList();

        final List<ObjectAction> sortedPossiblyMissingActions =
                _Lists.map(possiblyMissingActionIds, objectActionById::get);

        sortedPossiblyMissingActions.sort(ObjectMember.byMemberOrderSequence(false));

        final List<String> sortedPossiblyMissingActionIds =
                _Lists.map(sortedPossiblyMissingActions, ObjectMember::getId);

        for (final String actionId : sortedPossiblyMissingActionIds) {
            var objectAction = objectActionById.get(actionId);

            var layoutGroupFacet = objectAction.getFacet(LayoutGroupFacet.class);
            if(layoutGroupFacet == null) {
                continue;
            }
            final String layoutGroupName = layoutGroupFacet.getGroupId();
            if (layoutGroupName == null) {
                continue;
            }

            if (oneToOneAssociationById.containsKey(layoutGroupName)) {
                associatedActionIds.add(actionId);

                if(layoutGroupFacet.isExplicitBinding()) {
                    final PropertyLayoutData propertyLayoutData = propertyLayoutDataById.get(layoutGroupName);
                    if(propertyLayoutData == null) {
                        log.warn(String.format("Could not find propertyLayoutData for layoutGroupName of '%s'", layoutGroupName));
                        continue;
                    }
                    var actionLayoutData = new ActionLayoutData(actionId);
                    var actionPositionFacet = objectAction.getFacet(ActionPositionFacet.class);
                    final ActionLayoutDataOwner owner;
                    final ActionLayout.Position position;
                    if(actionPositionFacet != null) {
                        position = actionPositionFacet.position();
                        owner = position == ActionLayout.Position.PANEL
                                || position == ActionLayout.Position.PANEL_DROPDOWN
                                ? propertyLayoutData.getOwner()
                                : propertyLayoutData;
                    } else {
                        position = ActionLayout.Position.BELOW;
                        owner = propertyLayoutData;
                    }
                    actionLayoutData.setPosition(position);
                    addActionTo(owner, actionLayoutData);
                }

                continue;
            }
            if (oneToManyAssociationById.containsKey(layoutGroupName)) {
                associatedActionIds.add(actionId);

                if(layoutGroupFacet.isExplicitBinding()) {
                    var collectionLayoutData = collectionLayoutDataById.get(layoutGroupName);
                    if(collectionLayoutData==null) {
                        log.warn("failed to lookup CollectionLayoutData by layoutGroupName '{}'", layoutGroupName);
                    } else {
                        var actionLayoutData = new ActionLayoutData(actionId);
                        addActionTo(collectionLayoutData, actionLayoutData);
                    }
                }
                continue;
            }
            // if the @ActionLayout for the action references a field set (that has bound
            // associations), then don't mark it as missing, but instead explicitly add it to the
            // list of actions of that field-set.
            final Set<String> boundAssociationIds = boundAssociationIdsByFieldSetId.get(layoutGroupName);
            if(boundAssociationIds != null && !boundAssociationIds.isEmpty()) {

                associatedActionIds.add(actionId);

                final ActionLayoutData actionLayoutData = new ActionLayoutData(actionId);

                // since the action is to be associated with a fieldSet, the only available positions are PANEL and PANEL_DROPDOWN.
                // if the action already has a preference for PANEL, then preserve it, otherwise default to PANEL_DROPDOWN
                var actionPositionFacet = objectAction.getFacet(ActionPositionFacet.class);
                if(actionPositionFacet != null && actionPositionFacet.position() == ActionLayout.Position.PANEL) {
                    actionLayoutData.setPosition(ActionLayout.Position.PANEL);
                } else {
                    actionLayoutData.setPosition(ActionLayout.Position.PANEL_DROPDOWN);
                }

                final FieldSet fieldSet = gridModel.getFieldSet(layoutGroupName);
                addActionTo(fieldSet, actionLayoutData);
            }
        }

        // ... the missing actions are those in the second tuple, excluding those associated
        // (via @Action#associateWith) to a property or collection. (XXX comment might be outdated)
        final List<String> missingActionIds = _Lists.newArrayList(sortedPossiblyMissingActionIds);
        missingActionIds.removeAll(associatedActionIds);

        for (String surplusActionId : surplusActionIds) {
            actionLayoutDataById.get(surplusActionId).setMetadataError("No such action");
        }

        if(!missingActionIds.isEmpty()) {
            final BSCol bsCol = gridModel.getColForUnreferencedActionsRef();
            if(bsCol != null) {
                addActionsTo(
                        bsCol,
                        missingActionIds,
                        layoutDataFactory::createActionLayoutData,
                        actionLayoutDataById::put);
            } else {
                final FieldSet fieldSet = gridModel.getFieldSetForUnreferencedActionsRef();
                if(fieldSet != null) {
                    addActionsTo(
                            fieldSet,
                            missingActionIds,
                            layoutDataFactory::createActionLayoutData,
                            actionLayoutDataById::put);
                }
            }
        }

        return true;
    }