Table.java

/*-
 * #%L
 * JSQLParser library
 * %%
 * Copyright (C) 2004 - 2019 JSQLParser
 * %%
 * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
 * #L%
 */
package net.sf.jsqlparser.schema;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.MySQLIndexHint;
import net.sf.jsqlparser.expression.SQLServerHints;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
import net.sf.jsqlparser.statement.ErrorDestination;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.FromItemVisitor;
import net.sf.jsqlparser.statement.select.IntoTableVisitor;
import net.sf.jsqlparser.statement.select.Pivot;
import net.sf.jsqlparser.statement.select.SampleClause;
import net.sf.jsqlparser.statement.select.UnPivot;

/**
 * A table. It can have an alias and the schema name it belongs to.
 */
public class Table extends ASTNodeAccessImpl
        implements ErrorDestination, FromItem, MultiPartName, Cloneable {

    private static final int NAME_IDX = 0;

    private static final int SCHEMA_IDX = 1;

    private static final int DATABASE_IDX = 2;

    private static final int SERVER_IDX = 3;

    private List<String> partItems = new ArrayList<>();

    private List<String> partDelimiters = new ArrayList<>();

    // holds the various `time travel` syntax for BigQuery, RedShift, Snowflake or RedShift
    private String timeTravelStr = null;

    private Alias alias;

    // thank you, Google!
    private String timeTravelStrAfterAlias = null;

    private SampleClause sampleClause;

    private Pivot pivot;

    private UnPivot unpivot;

    private MySQLIndexHint mysqlHints;

    private SQLServerHints sqlServerHints;

    // holds the physical table when resolved against an actual schema information
    private Table resolvedTable = null;

    public Table() {}

    /**
     * Instantiates a new Table.
     *
     * Sets the table name, splitting it into parts (catalog, schema, name) on `.` dots when quoted
     * unless the system property `SPLIT_NAMES_ON_DELIMITER` points to `FALSE`
     *
     * @param name the table name, optionally quoted
     */
    public Table(String name) {
        setName(name);
    }

    public Table(String name, boolean splitNamesOnDelimiter) {
        setName(name, splitNamesOnDelimiter);
    }

    public Table(String schemaName, String name) {
        setSchemaName(schemaName);
        setName(name);
    }

    public Table(Database database, String schemaName, String name) {
        setDatabase(database);
        setSchemaName(schemaName);
        setName(name);
    }

    public Table(String catalogName, String schemaName, String tableName) {
        setSchemaName(schemaName);
        setDatabase(new Database(catalogName));
        setName(tableName);
    }

    public Table(List<String> partItems) {
        if (partItems.size() == 1) {
            setName(partItems.get(0));
        } else {
            this.partItems = new ArrayList<>(partItems);
            Collections.reverse(this.partItems);
        }
    }

    public Table(List<String> partItems, List<String> partDelimiters) {
        if (partItems.size() == 1) {
            setName(partItems.get(0));
        } else {
            if (partDelimiters.size() != partItems.size() - 1) {
                throw new IllegalArgumentException(
                        "the length of the delimiters list must be 1 less than nameParts");
            }
            this.partItems = new ArrayList<>(partItems);
            this.partDelimiters = new ArrayList<>(partDelimiters);
            Collections.reverse(this.partItems);
            Collections.reverse(this.partDelimiters);
        }
    }

    public String getCatalogName() {
        return getIndex(DATABASE_IDX);
    }

    public Database getDatabase() {
        return new Database(getIndex(DATABASE_IDX));
    }

    public String getDatabaseName() {
        return getIndex(DATABASE_IDX);
    }

    public String getUnquotedCatalogName() {
        return MultiPartName.unquote(getDatabaseName());
    }

    public String getUnquotedDatabaseName() {
        return MultiPartName.unquote(getDatabaseName());
    }

    public void setDatabase(Database database) {
        setIndex(DATABASE_IDX, database.getDatabaseName());
        if (database.getServer() != null) {
            setIndex(SERVER_IDX, database.getServer().getFullyQualifiedName());
        }
    }

    public Table setDatabaseName(String databaseName) {
        this.setDatabase(new Database(databaseName));
        return this;
    }

    public Table withDatabase(Database database) {
        setDatabase(database);
        return this;
    }

    public String getSchemaName() {
        return getIndex(SCHEMA_IDX);
    }

    public String getUnquotedSchemaName() {
        return MultiPartName.unquote(getSchemaName());
    }

    public Table setSchemaName(String schemaName) {
        this.setIndex(SCHEMA_IDX, schemaName);
        return this;
    }

    public Table withSchemaName(String schemaName) {
        setSchemaName(schemaName);
        return this;
    }

    public String getName() {
        String name = getIndex(NAME_IDX);
        if (name != null && name.contains("@")) {
            int pos = name.lastIndexOf('@');
            if (pos > 0) {
                name = name.substring(0, pos);
            }
        }
        return name;
    }


    /**
     * Sets the table name, splitting it into parts (catalog, schema, name) on `.` dots when quoted
     * unless the system property `SPLIT_NAMES_ON_DELIMITER` points to `FALSE`
     *
     * @param name the table name, optionally quoted
     */
    public void setName(String name) {
        // BigQuery seems to allow things like: `catalogName.schemaName.tableName` in only one pair
        // of quotes
        // however, some people believe that Dots in Names are a good idea, so provide a switch-off
        boolean splitNamesOnDelimiter = System.getProperty("SPLIT_NAMES_ON_DELIMITER") == null ||
                !List
                        .of("0", "N", "n", "FALSE", "false", "OFF", "off")
                        .contains(System.getProperty("SPLIT_NAMES_ON_DELIMITER"));

        setName(name, splitNamesOnDelimiter);
    }

    public void setName(String name, boolean splitNamesOnDelimiter) {
        if (MultiPartName.isQuoted(name) && name.contains(".") && splitNamesOnDelimiter) {
            partItems.clear();
            for (String unquotedIdentifier : MultiPartName.unquote(name).split("\\.")) {
                partItems.add("\"" + unquotedIdentifier + "\"");
            }
            Collections.reverse(partItems);
        } else if (name.contains(".") && splitNamesOnDelimiter) {
            partItems.clear();
            partItems.addAll(Arrays.asList(MultiPartName.unquote(name).split("\\.")));
            Collections.reverse(partItems);
        } else {
            setIndex(NAME_IDX, name);
        }
    }

    public String getDBLinkName() {
        String name = getIndex(NAME_IDX);
        if (name != null && name.contains("@")) {
            int pos = name.lastIndexOf('@');
            if (pos > 0) {
                name = name.substring(pos + 1);
            }
        }
        return name;
    }

    public Table withName(String name) {
        this.setName(name);
        return this;
    }

    @Override
    public Alias getAlias() {
        return alias;
    }

    @Override
    public void setAlias(Alias alias) {
        this.alias = alias;
    }

    public String getTimeTravelStrAfterAlias() {
        return timeTravelStrAfterAlias;
    }

    public Table setTimeTravelStrAfterAlias(String timeTravelStrAfterAlias) {
        this.timeTravelStrAfterAlias = timeTravelStrAfterAlias;
        return this;
    }

    private void setIndex(int idx, String value) {
        int size = partItems.size();
        for (int i = 0; i < idx - size + 1; i++) {
            partItems.add(null);
        }

        if (value == null && idx == partItems.size() - 1) {
            partItems.remove(idx);
        } else {
            partItems.set(idx, value);
        }
    }

    private String getIndex(int idx) {
        if (idx < partItems.size()) {
            return partItems.get(idx);
        } else {
            return null;
        }
    }

    @Override
    public String getFullyQualifiedName() {
        StringBuilder fqn = new StringBuilder();

        // remove any leading empty items
        // only middle items can be suppressed (e.g. dbo..MY_TABLE )
        while (!partItems.isEmpty() && (partItems.get(partItems.size() - 1) == null
                || partItems.get(partItems.size() - 1).isEmpty())) {
            partItems.remove(partItems.size() - 1);
        }

        for (int i = partItems.size() - 1; i >= 0; i--) {
            String part = partItems.get(i);
            if (part == null) {
                part = "";
            }
            fqn.append(part);
            if (i != 0) {
                fqn.append(partDelimiters.isEmpty() ? "." : partDelimiters.get(i - 1));
            }
        }

        return fqn.toString();
    }

    @Override
    public String getUnquotedName() {
        return MultiPartName.unquote(getName());
    }

    @Override
    public <T, S> T accept(FromItemVisitor<T> fromItemVisitor, S context) {
        return fromItemVisitor.visit(this, context);
    }

    public <T, S> T accept(IntoTableVisitor<T> intoTableVisitor, S context) {
        return intoTableVisitor.visit(this, context);
    }

    public String getTimeTravel() {
        return timeTravelStr;
    }

    public Table setTimeTravel(String timeTravelStr) {
        this.timeTravelStr = timeTravelStr;
        return this;
    }

    @Override
    public Pivot getPivot() {
        return pivot;
    }

    @Override
    public void setPivot(Pivot pivot) {
        this.pivot = pivot;
    }

    @Override
    public UnPivot getUnPivot() {
        return this.unpivot;
    }

    @Override
    public void setUnPivot(UnPivot unpivot) {
        this.unpivot = unpivot;
    }

    public MySQLIndexHint getIndexHint() {
        return mysqlHints;
    }

    public Table withHint(MySQLIndexHint hint) {
        setHint(hint);
        return this;
    }

    public void setHint(MySQLIndexHint hint) {
        this.mysqlHints = hint;
    }

    public SQLServerHints getSqlServerHints() {
        return sqlServerHints;
    }

    public void setSqlServerHints(SQLServerHints sqlServerHints) {
        this.sqlServerHints = sqlServerHints;
    }

    public SampleClause getSampleClause() {
        return sampleClause;
    }

    public Table setSampleClause(SampleClause sampleClause) {
        this.sampleClause = sampleClause;
        return this;
    }

    public StringBuilder appendTo(StringBuilder builder) {
        builder.append(getFullyQualifiedName());

        if (timeTravelStr != null) {
            builder.append(" ").append(timeTravelStr);
        }

        if (alias != null) {
            builder.append(alias);
        }

        if (timeTravelStrAfterAlias != null) {
            builder.append(" ").append(timeTravelStrAfterAlias);
        }

        if (sampleClause != null) {
            sampleClause.appendTo(builder);
        }

        if (pivot != null) {
            builder.append(" ").append(pivot);
        }

        if (unpivot != null) {
            builder.append(" ").append(unpivot);
        }

        if (mysqlHints != null) {
            builder.append(mysqlHints);
        }

        if (sqlServerHints != null) {
            builder.append(sqlServerHints);
        }
        return builder;
    }

    @Override
    public String toString() {
        return appendTo(new StringBuilder()).toString();
    }

    @Override
    public Table withUnPivot(UnPivot unpivot) {
        return (Table) FromItem.super.withUnPivot(unpivot);
    }

    @Override
    public Table withAlias(Alias alias) {
        return (Table) FromItem.super.withAlias(alias);
    }

    @Override
    public Table withPivot(Pivot pivot) {
        return (Table) FromItem.super.withPivot(pivot);
    }

    public Table withSqlServerHints(SQLServerHints sqlServerHints) {
        this.setSqlServerHints(sqlServerHints);
        return this;
    }

    public List<String> getNameParts() {
        return partItems;
    }

    public List<String> getNamePartDelimiters() {
        return partDelimiters;
    }

    /**
     * Gets the actual table when resolved against a physical schema information.
     *
     * @return the actual table when resolved against a physical schema information
     */
    public Table getResolvedTable() {
        return resolvedTable;
    }

    /**
     * Sets resolved table.
     *
     * @param resolvedTable the resolved table
     * @return this table
     */
    public Table setResolvedTable(Table resolvedTable) {
        // clone, not reference
        if (resolvedTable != null) {
            this.resolvedTable = new Table(resolvedTable.getFullyQualifiedName());
        }
        return this;
    }

    /**
     * Sets a table's catalog and schema only when not set. Useful for setting CURRENT_SCHEMA() and
     * CURRENT_DATABASE()
     *
     * @param currentCatalogName the catalog name
     * @param currentSchemaName the schema name
     * @return the provided table
     */
    public Table setUnsetCatalogAndSchema(String currentCatalogName, String currentSchemaName) {
        String databaseName = getDatabaseName();
        if (databaseName == null || databaseName.isEmpty()) {
            setDatabaseName(currentCatalogName);
        }

        String schemaName = getSchemaName();
        if (schemaName == null || schemaName.isEmpty()) {
            setSchemaName(currentSchemaName);
        }
        return this;
    }

    /**
     * Sets a tables' catalog and schema only when not set. Useful for setting CURRENT_SCHEMA() and
     * CURRENT_DATABASE()
     *
     * @param currentCatalogName the current catalog name
     * @param currentSchemaName the current schema name
     * @param tables the tables
     * @return the tables
     */
    public static Table[] setUnsetCatalogAndSchema(String currentCatalogName,
            String currentSchemaName, Table... tables) {
        for (Table t : tables) {
            if (t != null) {
                t.setUnsetCatalogAndSchema(currentCatalogName, currentSchemaName);
            }
        }
        return tables;
    }

    @Override
    public Table clone() {
        Table clone = new Table(this.getFullyQualifiedName());
        clone.setResolvedTable(this.resolvedTable != null ? this.resolvedTable.clone() : null);
        return clone;
    }
}