TableConfiguration.java

/*
 *    Copyright 2006-2026 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://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.mybatis.generator.config;

import static org.mybatis.generator.internal.util.StringUtility.composeFullyQualifiedTableName;
import static org.mybatis.generator.internal.util.StringUtility.isTrue;
import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;

import org.jspecify.annotations.Nullable;
import org.mybatis.generator.api.KnownRuntime;
import org.mybatis.generator.internal.util.messages.Messages;

public class TableConfiguration extends PropertyHolder {
    private final boolean insertStatementEnabled;
    private final boolean selectByPrimaryKeyStatementEnabled;
    private final boolean selectByExampleStatementEnabled;
    private final boolean updateByPrimaryKeyStatementEnabled;
    private final boolean deleteByPrimaryKeyStatementEnabled;
    private final boolean deleteByExampleStatementEnabled;
    private final boolean countByExampleStatementEnabled;
    private final boolean updateByExampleStatementEnabled;
    private final List<ColumnOverride> columnOverrides;
    // this is a Map for validation purposes. Initially, all items will be FALSE. When accessed, an item will
    // be made TRUE. This allows us to generate warning for columns configured to be ignored but not found.
    private final Map<IgnoredColumn, Boolean> ignoredColumns;
    private final @Nullable GeneratedKey generatedKey;
    private final @Nullable String catalog;
    private final @Nullable String schema;
    private final String tableName;
    private final @Nullable String domainObjectName;
    private final @Nullable String alias;
    private final @Nullable ModelType modelType;
    private final boolean wildcardEscapingEnabled;
    private final boolean delimitIdentifiers;
    private final @Nullable DomainObjectRenamingRule domainObjectRenamingRule;
    private final @Nullable ColumnRenamingRule columnRenamingRule;
    private final boolean isAllColumnDelimitingEnabled;
    private final @Nullable String mapperName;
    private final @Nullable String sqlProviderName;
    private final List<IgnoredColumnPattern> ignoredColumnPatterns;
    private final String fullyQualifiedName;

    protected TableConfiguration(Builder builder) {
        super(builder);

        catalog = builder.catalog;
        schema = builder.schema;
        tableName = Objects.requireNonNull(builder.tableName);
        domainObjectName = builder.domainObjectName;
        alias = builder.alias;
        modelType = builder.modelType;
        insertStatementEnabled = builder.insertStatementEnabled;
        selectByPrimaryKeyStatementEnabled = builder.selectByPrimaryKeyStatementEnabled;
        selectByExampleStatementEnabled = builder.selectByExampleStatementEnabled;
        updateByPrimaryKeyStatementEnabled = builder.updateByPrimaryKeyStatementEnabled;
        deleteByPrimaryKeyStatementEnabled = builder.deleteByPrimaryKeyStatementEnabled;
        deleteByExampleStatementEnabled = builder.deleteByExampleStatementEnabled;
        countByExampleStatementEnabled = builder.countByExampleStatementEnabled;
        updateByExampleStatementEnabled = builder.updateByExampleStatementEnabled;
        wildcardEscapingEnabled = builder.wildcardEscapingEnabled;
        delimitIdentifiers = builder.delimitIdentifiers;
        isAllColumnDelimitingEnabled = builder.isAllColumnDelimitingEnabled;
        mapperName = builder.mapperName;
        sqlProviderName = builder.sqlProviderName;
        columnOverrides = Collections.unmodifiableList(builder.columnOverrides);
        ignoredColumns = builder.ignoredColumns;
        generatedKey = builder.generatedKey;
        domainObjectRenamingRule = builder.domainObjectRenamingRule;
        columnRenamingRule = builder.columnRenamingRule;
        ignoredColumnPatterns = Collections.unmodifiableList(builder.ignoredColumnPatterns);
        fullyQualifiedName = composeFullyQualifiedTableName(catalog, schema, tableName, '.');
    }

    public boolean isDeleteByPrimaryKeyStatementEnabled() {
        return deleteByPrimaryKeyStatementEnabled;
    }

    public boolean isInsertStatementEnabled() {
        return insertStatementEnabled;
    }

    public boolean isSelectByPrimaryKeyStatementEnabled() {
        return selectByPrimaryKeyStatementEnabled;
    }

    public boolean isUpdateByPrimaryKeyStatementEnabled() {
        return updateByPrimaryKeyStatementEnabled;
    }

    public boolean isColumnIgnored(String columnName) {
        for (Map.Entry<IgnoredColumn, Boolean> entry : ignoredColumns.entrySet()) {
            if (entry.getKey().matches(columnName)) {
                entry.setValue(Boolean.TRUE);
                return true;
            }
        }

        for (IgnoredColumnPattern ignoredColumnPattern : ignoredColumnPatterns) {
            if (ignoredColumnPattern.matches(columnName)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof TableConfiguration other)) {
            return false;
        }

        return Objects.equals(this.catalog, other.catalog)
                && Objects.equals(this.schema, other.schema)
                && Objects.equals(this.tableName, other.tableName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(catalog, schema, tableName);
    }

    public boolean isSelectByExampleStatementEnabled() {
        return selectByExampleStatementEnabled;
    }

    /**
     * May return null if the column has not been overridden.
     *
     * @param columnName
     *            the column name
     * @return the column override (if any) related to this column
     */
    public Optional<ColumnOverride> getColumnOverride(String columnName) {
        for (ColumnOverride co : columnOverrides) {
            if (co.isColumnNameDelimited()) {
                if (columnName.equals(co.getColumnName())) {
                    return Optional.of(co);
                }
            } else {
                if (columnName.equalsIgnoreCase(co.getColumnName())) {
                    return Optional.of(co);
                }
            }
        }

        return Optional.empty();
    }

    public Optional<GeneratedKey> getGeneratedKey() {
        return Optional.ofNullable(generatedKey);
    }

    public boolean isDeleteByExampleStatementEnabled() {
        return deleteByExampleStatementEnabled;
    }

    public boolean areAnyStatementsEnabled() {
        return selectByExampleStatementEnabled
                || selectByPrimaryKeyStatementEnabled || insertStatementEnabled
                || updateByPrimaryKeyStatementEnabled
                || deleteByExampleStatementEnabled
                || deleteByPrimaryKeyStatementEnabled
                || countByExampleStatementEnabled
                || updateByExampleStatementEnabled;
    }

    public @Nullable String getAlias() {
        return alias;
    }

    public @Nullable String getCatalog() {
        return catalog;
    }

    public @Nullable String getDomainObjectName() {
        return domainObjectName;
    }

    public @Nullable String getSchema() {
        return schema;
    }

    public String getTableName() {
        return tableName;
    }

    public String getFullyQualifiedName() {
        return fullyQualifiedName;
    }

    public List<ColumnOverride> getColumnOverrides() {
        return columnOverrides;
    }

    /**
     * Returns a List of Strings. The values are the columns
     * that were specified to be ignored in the table but do not exist in the
     * table.
     *
     * @return a List of Strings - the columns that were improperly configured
     *         as ignored columns
     */
    public List<String> getIgnoredColumnsInError() {
        List<String> answer = new ArrayList<>();

        for (Map.Entry<IgnoredColumn, Boolean> entry : ignoredColumns.entrySet()) {
            if (Boolean.FALSE.equals(entry.getValue())) {
                answer.add(entry.getKey().getColumnName());
            }
        }

        return answer;
    }

    public Optional<ModelType> getModelType() {
        return Optional.ofNullable(modelType);
    }

    public boolean isWildcardEscapingEnabled() {
        return wildcardEscapingEnabled;
    }

    @Override
    public String toString() {
        return fullyQualifiedName;
    }

    public boolean isDelimitIdentifiers() {
        return delimitIdentifiers;
    }

    public boolean isCountByExampleStatementEnabled() {
        return countByExampleStatementEnabled;
    }

    public boolean isUpdateByExampleStatementEnabled() {
        return updateByExampleStatementEnabled;
    }

    public void validate(List<String> errors, int listPosition, Context context, KnownRuntime knownRuntime) {
        if (!stringHasValue(tableName)) {
            errors.add(Messages.getString(
                    "ValidationError.6", Integer.toString(listPosition))); //$NON-NLS-1$
        }

        if (generatedKey != null) {
            // if the model type is immutable or record, then we cannot have generated keys
            ModelType mt = getModelType().orElseGet(context::getDefaultModelType);
            if (mt == ModelType.RECORD) {
                errors.add(Messages.getString("ValidationError.30", fullyQualifiedName, context.getId(), //$NON-NLS-1$
                        "record")); //$NON-NLS-1$
            }

            // we're going to allow generated keys for Kotlin even if the rest of the model is immutable
            if (isImmutable(context) && knownRuntime != KnownRuntime.MYBATIS3_KOTLIN) {
                errors.add(Messages.getString("ValidationError.30", fullyQualifiedName, context.getId(), //$NON-NLS-1$
                        "immutable")); //$NON-NLS-1$
            }

            generatedKey.validate(errors, fullyQualifiedName, context.getId());
        }

        if (domainObjectRenamingRule != null) {
            domainObjectRenamingRule.validate(errors, fullyQualifiedName);
        }

        if (columnRenamingRule != null) {
            columnRenamingRule.validate(errors, fullyQualifiedName);
        }

        for (ColumnOverride columnOverride : columnOverrides) {
            columnOverride.validate(errors, fullyQualifiedName);
        }

        for (IgnoredColumn ignoredColumn : ignoredColumns.keySet()) {
            ignoredColumn.validate(errors, fullyQualifiedName);
        }

        for (IgnoredColumnPattern ignoredColumnPattern : ignoredColumnPatterns) {
            ignoredColumnPattern.validate(errors, fullyQualifiedName);
        }
    }

    public boolean isImmutable(Context context) {
        Properties properties;

        if (getProperties().containsKey(PropertyRegistry.ANY_IMMUTABLE)) {
            properties = getProperties();
        } else {
            properties = context.getModelGeneratorConfiguration().getProperties();
        }

        return isTrue(properties.getProperty(PropertyRegistry.ANY_IMMUTABLE));
    }

    public boolean generateKotlinV1Model(Context context) {
        Properties properties;

        if (getProperties().containsKey(PropertyRegistry.GENERATE_KOTLIN_V1_MODEL)) {
            properties = getProperties();
        } else {
            properties = context.getModelGeneratorConfiguration().getProperties();
        }

        return isTrue(properties.getProperty(PropertyRegistry.GENERATE_KOTLIN_V1_MODEL));
    }

    public @Nullable DomainObjectRenamingRule getDomainObjectRenamingRule() {
        return domainObjectRenamingRule;
    }

    public Optional<ColumnRenamingRule> getColumnRenamingRule() {
        return Optional.ofNullable(columnRenamingRule);
    }

    public boolean isAllColumnDelimitingEnabled() {
        return isAllColumnDelimitingEnabled;
    }

    public @Nullable String getMapperName() {
        return mapperName;
    }

    public @Nullable String getSqlProviderName() {
        return sqlProviderName;
    }

    public @Nullable String getDynamicSqlSupportClassName() {
        return getProperty(PropertyRegistry.TABLE_DYNAMIC_SQL_SUPPORT_CLASS_NAME);
    }

    public @Nullable String getDynamicSqlTableObjectName() {
        return getProperty(PropertyRegistry.TABLE_DYNAMIC_SQL_TABLE_OBJECT_NAME);
    }

    public static class Builder extends AbstractBuilder<Builder> {
        private @Nullable ModelType modelType;
        private @Nullable String catalog;
        private @Nullable String schema;
        private @Nullable String tableName;
        private @Nullable String domainObjectName;
        private @Nullable String alias;
        private boolean insertStatementEnabled = true;
        private boolean selectByPrimaryKeyStatementEnabled = true;
        private boolean selectByExampleStatementEnabled = true;
        private boolean updateByPrimaryKeyStatementEnabled = true;
        private boolean deleteByPrimaryKeyStatementEnabled = true;
        private boolean deleteByExampleStatementEnabled = true;
        private boolean countByExampleStatementEnabled = true;
        private boolean updateByExampleStatementEnabled = true;
        private boolean wildcardEscapingEnabled;
        private boolean delimitIdentifiers;
        private boolean isAllColumnDelimitingEnabled;
        private @Nullable String mapperName;
        private @Nullable String sqlProviderName;
        private @Nullable GeneratedKey generatedKey;
        private @Nullable DomainObjectRenamingRule domainObjectRenamingRule;
        private @Nullable ColumnRenamingRule columnRenamingRule;
        private final List<IgnoredColumnPattern> ignoredColumnPatterns = new ArrayList<>();
        private final List<ColumnOverride> columnOverrides = new ArrayList<>();
        private final Map<IgnoredColumn, Boolean> ignoredColumns = new HashMap<>();

        public TableConfiguration build() {
            return new TableConfiguration(this);
        }

        @Override
        protected Builder getThis() {
            return this;
        }

        public Builder withModelType(@Nullable String tableModelType) {
            this.modelType = tableModelType == null ? null : ModelType.getModelType(tableModelType);
            return getThis();
        }

        public Builder withCatalog(@Nullable String catalog) {
            this.catalog = catalog;
            return this;
        }

        public Builder withSchema(@Nullable String schema) {
            this.schema = schema;
            return this;
        }

        public Builder withTableName(@Nullable String tableName) {
            this.tableName = tableName;
            return this;
        }

        public Builder withDomainObjectName(@Nullable String domainObjectName) {
            this.domainObjectName = domainObjectName;
            return this;
        }

        public Builder withAlias(@Nullable String alias) {
            this.alias = alias;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withInsertStatementEnabled(boolean insertStatementEnabled) {
            this.insertStatementEnabled = insertStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withSelectByPrimaryKeyStatementEnabled(boolean selectByPrimaryKeyStatementEnabled) {
            this.selectByPrimaryKeyStatementEnabled = selectByPrimaryKeyStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withSelectByExampleStatementEnabled(boolean selectByExampleStatementEnabled) {
            this.selectByExampleStatementEnabled = selectByExampleStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withUpdateByPrimaryKeyStatementEnabled(boolean updateByPrimaryKeyStatementEnabled) {
            this.updateByPrimaryKeyStatementEnabled = updateByPrimaryKeyStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withDeleteByPrimaryKeyStatementEnabled(boolean deleteByPrimaryKeyStatementEnabled) {
            this.deleteByPrimaryKeyStatementEnabled = deleteByPrimaryKeyStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withDeleteByExampleStatementEnabled(boolean deleteByExampleStatementEnabled) {
            this.deleteByExampleStatementEnabled = deleteByExampleStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withCountByExampleStatementEnabled(boolean countByExampleStatementEnabled) {
            this.countByExampleStatementEnabled = countByExampleStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withUpdateByExampleStatementEnabled(boolean updateByExampleStatementEnabled) {
            this.updateByExampleStatementEnabled = updateByExampleStatementEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withWildcardEscapingEnabled(boolean wildcardEscapingEnabled) {
            this.wildcardEscapingEnabled = wildcardEscapingEnabled;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withDelimitIdentifiers(boolean delimitIdentifiers) {
            this.delimitIdentifiers = delimitIdentifiers;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withAllColumnDelimitingEnabled(boolean isAllColumnDelimitingEnabled) {
            this.isAllColumnDelimitingEnabled = isAllColumnDelimitingEnabled;
            return this;
        }

        public Builder withMapperName(@Nullable String mapperName) {
            this.mapperName = mapperName;
            return this;
        }

        public Builder withSqlProviderName(@Nullable String sqlProviderName) {
            this.sqlProviderName = sqlProviderName;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withGeneratedKey(@Nullable GeneratedKey generatedKey) {
            this.generatedKey = generatedKey;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withDomainObjectRenamingRule(@Nullable DomainObjectRenamingRule domainObjectRenamingRule) {
            this.domainObjectRenamingRule = domainObjectRenamingRule;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withColumnRenamingRule(@Nullable ColumnRenamingRule columnRenamingRule) {
            this.columnRenamingRule = columnRenamingRule;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withIgnoredColumnPattern(IgnoredColumnPattern ignoredColumnPattern) {
            this.ignoredColumnPatterns.add(ignoredColumnPattern);
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withIgnoredColumn(IgnoredColumn ignoredColumn) {
            this.ignoredColumns.put(ignoredColumn, Boolean.FALSE);
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public Builder withColumnOverride(ColumnOverride columnOverride) {
            this.columnOverrides.add(columnOverride);
            return this;
        }
    }
}