de.metas.ui.web.window.descriptor.DocumentFieldDescriptor.java Source code

Java tutorial

Introduction

Here is the source code for de.metas.ui.web.window.descriptor.DocumentFieldDescriptor.java

Source

package de.metas.ui.web.window.descriptor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;

import javax.annotation.Nullable;

import org.adempiere.ad.expression.api.ConstantLogicExpression;
import org.adempiere.ad.expression.api.IExpression;
import org.adempiere.ad.expression.api.ILogicExpression;
import org.adempiere.ad.expression.api.impl.LogicExpressionCompiler;
import org.adempiere.exceptions.AdempiereException;
import org.slf4j.Logger;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;

import de.metas.i18n.ITranslatableString;
import de.metas.i18n.ImmutableTranslatableString;
import de.metas.logging.LogManager;
import de.metas.ui.web.window.WindowConstants;
import de.metas.ui.web.window.datatypes.DataTypes;
import de.metas.ui.web.window.datatypes.LookupValue.IntegerLookupValue;
import de.metas.ui.web.window.datatypes.LookupValue.StringLookupValue;
import de.metas.ui.web.window.descriptor.DocumentFieldDependencyMap.DependencyType;
import de.metas.ui.web.window.descriptor.DocumentLayoutElementFieldDescriptor.LookupSource;
import de.metas.ui.web.window.descriptor.LookupDescriptorProvider.LookupScope;
import de.metas.ui.web.window.model.IDocumentFieldValueProvider;
import de.metas.ui.web.window.model.lookup.LookupDataSource;
import de.metas.ui.web.window.model.lookup.LookupDataSourceFactory;
import de.metas.ui.web.window.model.lookup.LookupValueByIdSupplier;
import de.metas.util.Check;
import lombok.Getter;
import lombok.NonNull;

/*
 * #%L
 * metasfresh-webui-api
 * %%
 * Copyright (C) 2016 metas GmbH
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

public final class DocumentFieldDescriptor {
    public static final Builder builder(final String fieldName) {
        return new Builder(fieldName);
    }

    private static final Logger logger = LogManager.getLogger(DocumentFieldDescriptor.class);

    /** Internal field name (aka ColumnName) */
    private final String fieldName;
    private final ITranslatableString caption;
    private final ITranslatableString description;
    /** Detail ID or null if this is a field in main sections */
    private final DetailId detailId;

    /** Is this the key field ? */
    private final boolean key;
    private final boolean calculated;

    @Getter
    private final boolean parentLink;
    @Getter
    private final String parentLinkFieldName;

    private final DocumentFieldWidgetType widgetType;
    private final boolean allowShowPassword; // in case widgetType is Password
    private final ButtonFieldActionDescriptor buttonActionDescriptor;

    private final WidgetSize widgetSize;

    private final Class<?> valueClass;

    private final LookupDescriptorProvider lookupDescriptorProvider;
    private final boolean supportZoomInto;

    private final boolean virtualField;
    private final Optional<IDocumentFieldValueProvider> virtualFieldValueProvider;

    private final Optional<IExpression<?>> defaultValueExpression;
    private final ImmutableList<IDocumentFieldCallout> callouts;

    public static enum Characteristic {
        PublicField //
        , AdvancedField //
        , SideListField //
        , GridViewField //
        //
        , SpecialField_DocumentNo //
        , SpecialField_DocStatus //
        , SpecialField_DocAction //
        // , SpecialField_DocumentSummary //
        ;
    };

    private static final List<Characteristic> SPECIALFIELDS_ToExcludeFromLayout = ImmutableList.of(
            // Characteristic.SpecialField_DocumentNo // NOP, don't exclude it (see https://github.com/metasfresh/metasfresh-webui-api/issues/291 )
            Characteristic.SpecialField_DocStatus //
            , Characteristic.SpecialField_DocAction //
    // , SpecialField_DocumentSummary // NOP, don't exclude DocumentSummary because if it's layout it shall be editable at least when new (e.g. C_BPartner.Name)
    );

    private final Set<Characteristic> characteristics;

    private final ILogicExpression readonlyLogic;
    private final boolean alwaysUpdateable;
    private final ILogicExpression displayLogic;
    private final ILogicExpression mandatoryLogic;

    private final Optional<DocumentFieldDataBindingDescriptor> dataBinding;

    private final DocumentFieldDependencyMap dependencies;

    //
    // Default filtering options
    private final DocumentFieldDefaultFilterDescriptor defaultFilterInfo;

    private DocumentFieldDescriptor(final Builder builder) {
        fieldName = Preconditions.checkNotNull(builder.fieldName, "name is null");
        caption = builder.getCaption();
        description = builder.getDescription();
        detailId = builder.getDetailId();

        key = builder.isKey();
        calculated = builder.isCalculated();

        parentLink = builder.parentLink;
        parentLinkFieldName = builder.parentLinkFieldName;

        widgetType = builder.getWidgetType();

        widgetSize = builder.getWidgetSize();
        allowShowPassword = builder.isAllowShowPassword();
        buttonActionDescriptor = builder.getButtonActionDescriptor();
        valueClass = builder.getValueClass();

        lookupDescriptorProvider = builder.getLookupDescriptorProvider();
        supportZoomInto = builder.isSupportZoomInto();

        defaultValueExpression = Preconditions.checkNotNull(builder.defaultValueExpression,
                "defaultValueExpression not null");

        virtualField = builder.isVirtualField();
        virtualFieldValueProvider = builder.getVirtualFieldValueProvider();

        characteristics = Sets.immutableEnumSet(builder.characteristics);
        readonlyLogic = builder.getReadonlyLogicEffective();
        alwaysUpdateable = builder.alwaysUpdateable;
        displayLogic = builder.displayLogic;
        mandatoryLogic = builder.getMandatoryLogicEffective();

        dataBinding = builder.getDataBinding();

        dependencies = builder.buildDependencies();

        callouts = builder.buildCallouts();

        //
        // Default filtering
        defaultFilterInfo = builder.defaultFilterInfo;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).omitNullValues().add("fieldName", fieldName)
                .add("detailId", detailId).add("widgetType", widgetType)
                .add("characteristics", characteristics.isEmpty() ? null : characteristics)
                .add("fieldDataBinding", dataBinding).toString();
    }

    public String getFieldName() {
        return fieldName;
    }

    public ITranslatableString getCaption() {
        return caption;
    }

    public ITranslatableString getDescription() {
        return description;
    }

    public DetailId getDetailId() {
        return detailId;
    }

    public boolean isKey() {
        return key;
    }

    public boolean isVirtualField() {
        return virtualField;
    }

    public Optional<IDocumentFieldValueProvider> getVirtualFieldValueProvider() {
        return virtualFieldValueProvider;
    }

    public boolean isCalculated() {
        return calculated;
    }

    public DocumentFieldWidgetType getWidgetType() {
        return widgetType;
    }

    public WidgetSize getWidgetSize() {
        return widgetSize;
    }

    public boolean isAllowShowPassword() {
        return allowShowPassword;
    }

    public ButtonFieldActionDescriptor getButtonActionDescriptor() {
        return buttonActionDescriptor;
    }

    public Class<?> getValueClass() {
        return valueClass;
    }

    public boolean isSupportZoomInto() {
        return supportZoomInto;
    }

    public LookupDescriptor getLookupDescriptor(final LookupScope scope) {
        return lookupDescriptorProvider.provideForScope(scope);
    }

    public LookupSource getLookupSourceType() {
        final LookupDescriptor lookupDescriptor = lookupDescriptorProvider
                .provideForScope(LookupScope.DocumentField);
        return lookupDescriptor == null ? null : lookupDescriptor.getLookupSourceType();
    }

    public Optional<String> getLookupTableName() {
        return extractLookupTableName(lookupDescriptorProvider);
    }

    private static final Optional<String> extractLookupTableName(
            final LookupDescriptorProvider lookupDescriptorProvider) {
        final LookupDescriptor lookupDescriptor = lookupDescriptorProvider
                .provideForScope(LookupScope.DocumentField);
        return lookupDescriptor == null ? Optional.empty() : lookupDescriptor.getTableName();
    }

    @Nullable
    public LookupDataSource createLookupDataSource(final LookupScope scope) {
        final LookupDescriptor lookupDescriptor = getLookupDescriptor(scope);
        if (lookupDescriptor == null) {
            return null;
        }

        return LookupDataSourceFactory.instance.getLookupDataSource(lookupDescriptor);

    }

    public Optional<IExpression<?>> getDefaultValueExpression() {
        return defaultValueExpression;
    }

    public boolean hasCharacteristic(final Characteristic c) {
        return characteristics.contains(c);
    }

    public ILogicExpression getReadonlyLogic() {
        return readonlyLogic;
    }

    public boolean isAlwaysUpdateable() {
        return alwaysUpdateable;
    }

    public ILogicExpression getDisplayLogic() {
        return displayLogic;
    }

    public ILogicExpression getMandatoryLogic() {
        return mandatoryLogic;
    }

    /**
     * @return field data binding info
     */
    public Optional<DocumentFieldDataBindingDescriptor> getDataBinding() {
        return dataBinding;
    }

    public <T extends DocumentFieldDataBindingDescriptor> T getDataBindingNotNull(final Class<T> bindingClass) {
        @SuppressWarnings("unchecked")
        final T dataBindingCasted = (T) dataBinding
                .orElseThrow(() -> new IllegalStateException("No databinding defined for " + this));
        return dataBindingCasted;
    }

    public DocumentFieldDependencyMap getDependencies() {
        return dependencies;
    }

    public Object convertToValueClass(final Object value, final LookupValueByIdSupplier lookupDataSource) {
        return DataTypes.convertToValueClass(fieldName, value, widgetType, valueClass, lookupDataSource);
    }

    /**
     * Converts given value to target class.
     *
     * @param value value to be converted
     * @param targetType target type
     * @param widgetType optional widget type
     * @param lookupDataSource optional Lookup data source, if needed
     * @return converted value
     */
    public <T> T convertToValueClass(final Object value, final DocumentFieldWidgetType widgetType,
            final Class<T> targetType, final LookupValueByIdSupplier lookupDataSource) {
        return DataTypes.convertToValueClass(fieldName, value, widgetType, targetType, lookupDataSource);
    }

    /* package */List<IDocumentFieldCallout> getCallouts() {
        return callouts;
    }

    public boolean isDefaultFilterField() {
        return defaultFilterInfo != null;
    }

    public DocumentFieldDefaultFilterDescriptor getDefaultFilterInfo() {
        return defaultFilterInfo;
    }

    /**
     * Builder
     */
    public static final class Builder {
        private DocumentFieldDescriptor _fieldBuilt;

        private final String fieldName;
        private ITranslatableString caption;
        private ITranslatableString description;
        public DetailId _detailId;

        private boolean key = false;
        private boolean parentLink = false;
        private String parentLinkFieldName;
        private boolean virtualField;
        private Optional<IDocumentFieldValueProvider> virtualFieldValueProvider = Optional.empty();
        private boolean calculated;

        private DocumentFieldWidgetType _widgetType;
        private WidgetSize _widgetSize;
        private Class<?> _valueClass;
        private boolean _allowShowPassword = false; // in case widgetType is Password

        // Lookup
        private LookupDescriptorProvider lookupDescriptorProvider = LookupDescriptorProvider.NULL;

        private Optional<IExpression<?>> defaultValueExpression = Optional.empty();

        private final Set<Characteristic> characteristics = new TreeSet<>();
        private ILogicExpression _entityReadonlyLogic = ConstantLogicExpression.FALSE;
        private ILogicExpression _readonlyLogic = ConstantLogicExpression.FALSE;
        private ILogicExpression _readonlyLogicEffective = null;

        private boolean alwaysUpdateable = false;
        private ILogicExpression displayLogic = ConstantLogicExpression.TRUE;
        private ILogicExpression _mandatoryLogic = ConstantLogicExpression.FALSE;
        private ILogicExpression _mandatoryLogicEffective = null;

        private Optional<DocumentFieldDataBindingDescriptor> _dataBinding = Optional.empty();

        private final List<IDocumentFieldCallout> callouts = new ArrayList<>();

        private ButtonFieldActionDescriptor buttonActionDescriptor = null;

        /** See {@link #setTooltipIconName(String)}. */
        @Getter
        private String tooltipIconName = null;

        //
        // Default filtering options
        private DocumentFieldDefaultFilterDescriptor defaultFilterInfo = null;

        private Builder(final String fieldName) {
            Check.assumeNotEmpty(fieldName, "fieldName is not empty");
            this.fieldName = fieldName;
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).omitNullValues().add("name", fieldName)
                    .add("detailId", _detailId).add("widgetType", _widgetType)
                    .add("characteristics", characteristics.isEmpty() ? null : characteristics).toString();
        }

        public DocumentFieldDescriptor getOrBuild() {
            if (_fieldBuilt == null) {
                _fieldBuilt = new DocumentFieldDescriptor(this);
            }
            return _fieldBuilt;
        }

        private final void assertNotBuilt() {
            if (_fieldBuilt != null) {
                throw new IllegalStateException("Already built: " + this);
            }
        }

        public String getFieldName() {
            return fieldName;
        }

        public Builder setCaption(final Map<String, String> captionTrls, final String defaultCaption) {
            caption = ImmutableTranslatableString.ofMap(captionTrls, defaultCaption);
            return this;
        }

        public Builder setCaption(final ITranslatableString caption) {
            this.caption = caption;
            return this;
        }

        public Builder setCaption(final String caption) {
            this.caption = ImmutableTranslatableString.constant(caption);
            return this;
        }

        public ITranslatableString getCaption() {
            if (caption == null) {
                return ImmutableTranslatableString.constant(fieldName);
            }

            return caption;
        }

        public Builder setDescription(final Map<String, String> descriptionTrls, final String defaultDescription) {
            description = ImmutableTranslatableString.ofMap(descriptionTrls, defaultDescription);
            return this;
        }

        public Builder setDescription(final ITranslatableString description) {
            this.description = description;
            return this;
        }

        public Builder setDescription(final String description) {
            this.description = ImmutableTranslatableString.constant(description);
            return this;
        }

        public ITranslatableString getDescription() {
            if (description == null) {
                return ImmutableTranslatableString.constant("");
            }
            return description;
        }

        /* package */Builder setDetailId(final DetailId detailId) {
            assertNotBuilt();
            _detailId = detailId;
            return this;
        }

        private DetailId getDetailId() {
            return _detailId;
        }

        /**
         * @return true if included entity (i.e. detail tab)
         */
        private boolean isDetail() {
            return getDetailId() != null;
        }

        public Builder setKey(final boolean key) {
            assertNotBuilt();
            this.key = key;
            return this;
        }

        boolean isKey() {
            return key;
        }

        public Builder setParentLink(final boolean parentLink, final String parentLinkFieldName) {
            assertNotBuilt();
            this.parentLink = parentLink;
            this.parentLinkFieldName = parentLinkFieldName;
            return this;
        }

        private boolean isParentLinkEffective() {
            return parentLink && isDetail();
        }

        public Builder setVirtualField(final boolean virtualField) {
            assertNotBuilt();
            this.virtualField = virtualField;
            virtualFieldValueProvider = Optional.empty();
            return this;
        }

        public Builder setVirtualField(@NonNull final IDocumentFieldValueProvider virtualFieldValueProvider) {
            assertNotBuilt();
            virtualField = true;
            this.virtualFieldValueProvider = Optional.of(virtualFieldValueProvider);
            return this;
        }

        public boolean isVirtualField() {
            return virtualField;
        }

        private Optional<IDocumentFieldValueProvider> getVirtualFieldValueProvider() {
            return virtualFieldValueProvider;
        }

        public Builder setCalculated(final boolean calculated) {
            assertNotBuilt();
            this.calculated = calculated;
            return this;
        }

        private boolean isCalculated() {
            if (isVirtualField()) {
                return true;
            }
            return calculated;
        }

        public Builder setWidgetType(final DocumentFieldWidgetType widgetType) {
            assertNotBuilt();
            _widgetType = widgetType;
            return this;
        }

        public DocumentFieldWidgetType getWidgetType() {
            Preconditions.checkNotNull(_widgetType, "widgetType is null");
            return _widgetType;
        }

        public WidgetSize getWidgetSize() {
            return _widgetSize;
        }

        public Builder setAllowShowPassword(final boolean allowShowPassword) {
            this._allowShowPassword = allowShowPassword;
            return this;
        }

        private boolean isAllowShowPassword() {
            return _allowShowPassword;
        }

        public Builder setLookupDescriptorProvider(final LookupDescriptorProvider lookupDescriptorProvider) {
            Check.assumeNotNull(lookupDescriptorProvider, "Parameter lookupDescriptorProvider is not null");
            this.lookupDescriptorProvider = lookupDescriptorProvider;
            return this;
        }

        public Builder setLookupDescriptorProvider(@Nullable final LookupDescriptor lookupDescriptor) {
            final LookupDescriptorProvider provider = lookupDescriptor != null
                    ? LookupDescriptorProvider.singleton(lookupDescriptor)
                    : LookupDescriptorProvider.NULL;
            setLookupDescriptorProvider(provider);
            return this;
        }

        public Builder setLookupDescriptorProvider_None() {
            setLookupDescriptorProvider(LookupDescriptorProvider.NULL);
            return this;
        }

        private LookupDescriptorProvider getLookupDescriptorProvider() {
            return lookupDescriptorProvider;
        }

        public LookupSource getLookupSourceType() {
            final LookupDescriptor lookupDescriptor = lookupDescriptorProvider
                    .provideForScope(LookupScope.DocumentField);
            return lookupDescriptor == null ? null : lookupDescriptor.getLookupSourceType();
        }

        public Optional<String> getLookupTableName() {
            return extractLookupTableName(lookupDescriptorProvider);
        }

        public Builder setValueClass(final Class<?> valueClass) {
            assertNotBuilt();
            _valueClass = valueClass;
            return this;
        }

        private Class<?> getValueClass() {
            if (_valueClass != null) {
                return _valueClass;
            }

            final DocumentFieldWidgetType widgetType = getWidgetType();
            if (widgetType.getValueClassOrNull() != null) {
                return widgetType.getValueClassOrNull();
            }

            final LookupDescriptorProvider lookupDescriptor = getLookupDescriptorProvider();
            if (lookupDescriptor != null) {
                return lookupDescriptor.isNumericKey() ? IntegerLookupValue.class : StringLookupValue.class;
            }

            throw new AdempiereException("valueClass is unknown for " + this);
        }

        public Builder setDefaultValueExpression(final Optional<IExpression<?>> defaultValueExpression) {
            assertNotBuilt();
            this.defaultValueExpression = Preconditions.checkNotNull(defaultValueExpression);
            return this;
        }

        public Builder setDefaultValueExpression(final IExpression<?> defaultValueExpression) {
            assertNotBuilt();
            this.defaultValueExpression = Optional.of(defaultValueExpression);
            return this;
        }

        public Builder addCharacteristic(final Characteristic c) {
            assertNotBuilt();
            characteristics.add(c);
            return this;
        }

        public boolean hasCharacteristic(final Characteristic c) {
            return characteristics.contains(c);
        }

        public Builder addCharacteristicIfTrue(final boolean test, final Characteristic c) {
            if (test) {
                addCharacteristic(c);
            }

            return this;
        }

        public Builder removeCharacteristic(final Characteristic c) {
            assertNotBuilt();
            characteristics.remove(c);
            return this;
        }

        public boolean isSpecialFieldToExcludeFromLayout() {
            return !Collections.disjoint(characteristics, SPECIALFIELDS_ToExcludeFromLayout);
        }

        /* package */ void setEntityReadonlyLogic(final ILogicExpression entityReadonlyLogic) {
            _entityReadonlyLogic = entityReadonlyLogic;
        }

        private ILogicExpression getEntityReadonlyLogic() {
            return _entityReadonlyLogic;
        }

        public Builder setReadonlyLogic(final ILogicExpression readonlyLogic) {
            assertNotBuilt();
            _readonlyLogic = Preconditions.checkNotNull(readonlyLogic);
            return this;
        }

        public Builder setReadonlyLogic(final boolean readonly) {
            setReadonlyLogic(ConstantLogicExpression.of(readonly));
            return this;
        }

        private ILogicExpression getReadonlyLogic() {
            return _readonlyLogic;
        }

        public ILogicExpression getReadonlyLogicEffective() {
            if (_readonlyLogicEffective == null) {
                _readonlyLogicEffective = buildReadonlyLogicEffective();
            }
            return _readonlyLogicEffective;
        }

        private ILogicExpression buildReadonlyLogicEffective() {
            if (isParentLinkEffective()) {
                return ConstantLogicExpression.TRUE;
            }

            if (isVirtualField()) {
                return ConstantLogicExpression.TRUE;
            }

            if (isKey()) {
                return ConstantLogicExpression.TRUE;
            }

            // If the tab is always readonly, we can assume any field in that tab is readonly
            final ILogicExpression entityReadonlyLogic = getEntityReadonlyLogic();
            if (entityReadonlyLogic.isConstantTrue()) {
                return ConstantLogicExpression.TRUE;
            }

            // Case: DocAction
            if (hasCharacteristic(Characteristic.SpecialField_DocAction)) {
                return ConstantLogicExpression.FALSE;
            }

            final ILogicExpression fieldReadonlyLogic = getReadonlyLogic();
            if (fieldReadonlyLogic.isConstantTrue()) {
                return ConstantLogicExpression.TRUE;
            }

            final String fieldName = getFieldName();
            if (WindowConstants.FIELDNAMES_CreatedUpdated.contains(fieldName)) {
                // NOTE: from UI perspective those are readonly (i.e. it will be managed by persistence layer)
                return ConstantLogicExpression.TRUE;
            }

            if (hasCharacteristic(Characteristic.SpecialField_DocStatus)) {
                // NOTE: DocStatus field shall always be readonly
                return ConstantLogicExpression.TRUE;
            }

            ILogicExpression readonlyLogic = fieldReadonlyLogic;
            // FIXME: not sure if using tabReadonlyLogic here is OK, because the tab logic shall be applied to parent tab!
            if (!entityReadonlyLogic.isConstantFalse()) {
                readonlyLogic = entityReadonlyLogic.or(fieldReadonlyLogic);
            }

            return readonlyLogic;
        }

        public Builder setAlwaysUpdateable(final boolean alwaysUpdateable) {
            assertNotBuilt();
            this.alwaysUpdateable = alwaysUpdateable;
            return this;
        }

        public boolean isAlwaysUpdateable() {
            return alwaysUpdateable;
        }

        public Builder setDisplayLogic(final ILogicExpression displayLogic) {
            assertNotBuilt();
            this.displayLogic = Preconditions.checkNotNull(displayLogic);
            return this;
        }

        public Builder setDisplayLogic(final boolean display) {
            setDisplayLogic(ConstantLogicExpression.of(display));
            return this;
        }

        public Builder setDisplayLogic(final String displayLogic) {
            setDisplayLogic(LogicExpressionCompiler.instance.compile(displayLogic));
            return this;
        }

        public ILogicExpression getDisplayLogic() {
            return displayLogic;
        }

        public boolean isPossiblePublicField() {
            // Always publish the key columns, else the client won't know what to talk about ;)
            if (isKey()) {
                return true;
            }

            // If display logic is not constant then we don't know if this field will be ever visible
            // so we are publishing it
            if (!displayLogic.isConstant()) {
                return true;
            }

            // Publish this field only if it's displayed
            return displayLogic.isConstantTrue();
        }

        public Builder setMandatoryLogic(final ILogicExpression mandatoryLogic) {
            assertNotBuilt();
            _mandatoryLogic = Preconditions.checkNotNull(mandatoryLogic);
            return this;
        }

        public Builder setMandatoryLogic(final boolean mandatory) {
            setMandatoryLogic(ConstantLogicExpression.of(mandatory));
            return this;
        }

        private ILogicExpression getMandatoryLogicEffective() {
            if (_mandatoryLogicEffective == null) {
                _mandatoryLogicEffective = buildMandatoryLogicEffective();
            }
            return _mandatoryLogicEffective;
        }

        private final ILogicExpression buildMandatoryLogicEffective() {
            if (isParentLinkEffective()) {
                return ConstantLogicExpression.TRUE;
            }

            final String fieldName = getFieldName();
            if (WindowConstants.FIELDNAMES_CreatedUpdated.contains(fieldName)) {
                // NOTE: from UI perspective those are not mandatory (i.e. it will be managed by persistence layer)
                return ConstantLogicExpression.FALSE;
            }

            if (isVirtualField()) {
                return ConstantLogicExpression.FALSE;
            }

            // FIXME: hardcoded M_AttributeSetInstance_ID mandatory logic = false
            // Reason: even if we set it's default value to "0" some callouts are setting it to NULL,
            // and then the document saving API is failing because it considers this column as NOT filled.
            if (WindowConstants.FIELDNAME_M_AttributeSetInstance_ID.equals(fieldName)) {
                return ConstantLogicExpression.FALSE;
            }

            // Corner case:
            // e.g. C_Order.M_Shipper_ID has AD_Field.IsMandatory=Y, AD_Field.IsDisplayed=N, AD_Column.IsMandatory=N
            // => we need to NOT enforce setting it because it's not needed, user cannot change it and it might be no callouts to set it.
            // Else, we won't be able to save our document.
            final boolean publicField = hasCharacteristic(Characteristic.PublicField);
            final ILogicExpression mandatoryLogic = _mandatoryLogic;
            final boolean mandatory = mandatoryLogic.isConstantTrue();
            final DocumentFieldDataBindingDescriptor fieldDataBinding = getDataBinding().orElse(null);
            final boolean mandatoryDB = fieldDataBinding != null && fieldDataBinding.isMandatory();
            if (!publicField && mandatory && !mandatoryDB) {
                return ConstantLogicExpression.FALSE;
            }

            // Case: DocumentNo special field shall always be mandatory
            if (hasCharacteristic(Characteristic.SpecialField_DocumentNo)) {
                return ConstantLogicExpression.TRUE;
            }

            if (mandatory) {
                return ConstantLogicExpression.TRUE;
            }

            return mandatoryLogic;
        }

        public Builder setDataBinding(final DocumentFieldDataBindingDescriptor dataBinding) {
            assertNotBuilt();
            _dataBinding = Optional.ofNullable(dataBinding);
            return this;
        }

        private Optional<DocumentFieldDataBindingDescriptor> getDataBinding() {
            return _dataBinding;
        }

        private DocumentFieldDependencyMap buildDependencies() {
            final DocumentFieldDependencyMap.Builder dependencyMapBuilder = DocumentFieldDependencyMap.builder()
                    .add(fieldName, getReadonlyLogicEffective().getParameterNames(), DependencyType.ReadonlyLogic)
                    .add(fieldName, getDisplayLogic().getParameterNames(), DependencyType.DisplayLogic)
                    .add(fieldName, getMandatoryLogicEffective().getParameterNames(),
                            DependencyType.MandatoryLogic);

            final LookupDescriptor lookupDescriptor = getLookupDescriptorProvider()
                    .provideForScope(LookupScope.DocumentField);
            if (lookupDescriptor != null) {
                dependencyMapBuilder.add(fieldName, lookupDescriptor.getDependsOnFieldNames(),
                        DependencyType.LookupValues);
            }

            final IDocumentFieldValueProvider virtualFieldValueProvider = getVirtualFieldValueProvider()
                    .orElse(null);
            if (virtualFieldValueProvider != null) {
                dependencyMapBuilder.add(fieldName, virtualFieldValueProvider.getDependsOnFieldNames(),
                        DependencyType.FieldValue);
            }

            return dependencyMapBuilder.build();
        }

        public Builder addCallout(final IDocumentFieldCallout callout) {
            Check.assumeNotNull(callout, "Parameter callout is not null");

            if (callouts.contains(callout)) {
                logger.warn("Skip adding {} because it was already added to {}", callout, this);
                return this;
            }
            callouts.add(callout);
            return this;
        }

        public Builder addCallout(final ILambdaDocumentFieldCallout lambdaCallout) {
            final LambdaDocumentFieldCallout callout = new LambdaDocumentFieldCallout(getFieldName(),
                    lambdaCallout);
            addCallout(callout);
            return this;
        }

        private ImmutableList<IDocumentFieldCallout> buildCallouts() {
            return ImmutableList.copyOf(callouts);
        }

        public Builder setButtonActionDescriptor(final ButtonFieldActionDescriptor buttonActionDescriptor) {
            this.buttonActionDescriptor = buttonActionDescriptor;
            return this;
        }

        public ButtonFieldActionDescriptor getButtonActionDescriptor() {
            return buttonActionDescriptor;
        }

        public boolean isSupportZoomInto() {
            // Allow zooming into key column. It shall open precisely this record in a new window
            // (see https://github.com/metasfresh/metasfresh/issues/1687 to understand the use-case)
            // In future we shall think to narrow it down only to included tabs and only for those tables which also have a window where they are the header document.
            if (isKey()) {
                return true;
            }

            final DocumentFieldWidgetType widgetType = getWidgetType();
            if (!widgetType.isSupportZoomInto()) {
                return false;
            }

            final Class<?> valueClass = getValueClass();
            if (StringLookupValue.class.isAssignableFrom(valueClass)) {
                return false;
            }

            final String lookupTableName = getLookupTableName().orElse(null);
            if (WindowConstants.TABLENAME_AD_Ref_List.equals(lookupTableName)) {
                return false;
            }

            return true;
        }

        public Builder setDefaultFilterInfo(DocumentFieldDefaultFilterDescriptor defaultFilterInfo) {
            this.defaultFilterInfo = defaultFilterInfo;
            return this;
        }

        /**
         * Setting this to a non-{@code null} value means that this field is a tooltip field,
         * i.e. it represents a tooltip that is attached to some other field.
         */
        public Builder setTooltipIconName(@Nullable final String tooltipIconName) {
            this.tooltipIconName = tooltipIconName;
            return this;
        }

        /**
         * @return true if this field has ORDER BY instructions
         */
        public boolean isDefaultOrderBy() {
            final DocumentFieldDataBindingDescriptor dataBinding = getDataBinding().orElse(null);
            return dataBinding != null ? dataBinding.isDefaultOrderBy() : false;
        }

        public int getDefaultOrderByPriority() {
            // we assume isDefaultOrderBy() was checked before calling this method
            return getDataBinding().get().getDefaultOrderByPriority();
        }

        public boolean isDefaultOrderByAscending() {
            // we assume isDefaultOrderBy() was checked before calling this method
            return getDataBinding().get().isDefaultOrderByAscending();
        }
    }
}