Skip to content
TapSchema.java 20.2 KiB
Newer Older
/* 
 * _____________________________________________________________________________
 * 
 * INAF - OATS National Institute for Astrophysics - Astronomical Observatory of
 * Trieste INAF - IA2 Italian Center for Astronomical Archives
 * _____________________________________________________________________________
 * 
 * Copyright (C) 2016 Istituto Nazionale di Astrofisica
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License Version 3 as published by the
 * Free Software Foundation.
 * 
 * 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, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package it.inaf.ia2.tsm;

import it.inaf.ia2.tsm.datalayer.DBBroker;
import it.inaf.ia2.tsm.datalayer.DBBrokerFactory;
import it.inaf.ia2.tsm.datalayer.DBWrapper;
import it.inaf.ia2.tsm.datalayer.DataTypeMode;
import it.inaf.ia2.tsm.model.ColumnModel;
import it.inaf.ia2.tsm.model.TableModel;
import it.inaf.ia2.tsm.model.SchemaModel;
import it.inaf.ia2.tsm.model.SchemaModels;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Sonia Zorba {@literal <zorba at oats.inaf.it>}
 */
public class TapSchema implements EntitiesContainer<Schema>, Serializable {

    public static final String STANDARD_NAME = "TAP_SCHEMA";

    // Mandatory tables constants
    public static final String TABLES_TABLE = "tables";
    public static final String SCHEMAS_TABLE = "schemas";
    public static final String COLUMNS_TABLE = "columns";
    public static final String KEYS_TABLE = "keys";
    public static final String KEY_COLUMNS_TABLE = "key_columns";
    public static final String DESCRIPTION_KEY = "description";

    private static final long serialVersionUID = 1678083091602571256L;

    private static final Logger LOG = LoggerFactory.getLogger(TapSchema.class);

    private final Map<String, Schema> schemas;
    private final Set<Key> allKeys;

    private boolean loading;
    private DBWrapper dbWrapper;
    private String dbName;
    private String name;
    private TapSchemaSettings settings;
    private DataTypeMode dataTypeMode;

    private transient DBBroker sourceDBBroker;
    private transient DBBroker tapSchemaDBBroker;

    private ConsistencyChecks consistencyChecks;

    public final DBBroker getSourceDBBroker() {
        if (sourceDBBroker == null) {
            sourceDBBroker = DBBrokerFactory.getDBBroker(dbWrapper.getSourceDataSourceWrapper(), dataTypeMode);
        }
        return sourceDBBroker;
    }

    public final DBBroker getTapSchemaDBBroker() {
        if (tapSchemaDBBroker == null) {
            tapSchemaDBBroker = DBBrokerFactory.getDBBroker(dbWrapper.getTapSchemaDataSourceWrapper(), dataTypeMode);
        }
        return tapSchemaDBBroker;
    }

    private TapSchema() {
        // for serialization
        schemas = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        allKeys = new HashSet<>();
        //consistencyChecks = new ConsistencyChecks();
    public TapSchema(DBWrapper dbWrapper, TapSchemaSettings settings, boolean exists) throws SQLException {
        this();

        this.dbWrapper = dbWrapper;
        this.exists = exists;
        // Don't change the instructions order!
        loadDBName();
        loadName();

        dataTypeMode = getTapSchemaModel().getDataTypeMode();
    private void loadDBName() {
        // Detect if the TAP_SCHEMA version supports dbmodel
        SchemaModel tapSchemaModel = SchemaModels.getTapSchemaModel(settings.getTapSchemaVersion());
        boolean hasDBName = tapSchemaModel.getTable(SCHEMAS_TABLE).get("dbname") != null;
        if (hasDBName && !STANDARD_NAME.equals(settings.getTapSchemaName())) {
            dbName = settings.getTapSchemaName();
        }
    }

    private void loadName() {
        if (dbName != null) {
            name = STANDARD_NAME;
        } else {
            name = settings.getTapSchemaName();
        }
    }

    public final void load() throws SQLException {

        loading = true;
        // Initializing schemas map
        schemas.clear();
        for (String schemaName : getSourceDBBroker().getAllSchemaNames()) {
            schemas.put(schemaName, null);
        }
        schemas.put(getName(), null); // the TAP_SCHEMA contains itself
            consistencyChecks = TapSchemaLoader.loadExistingTapSchema((this));
        checkKeys();
    public DBBroker getDBBroker(String schemaName) {
            return getTapSchemaDBBroker();
        } else {
            return getSourceDBBroker();
        }
    }

    /**
     * The name of the TAP_SCHEMA schema.
     */
    public final String getName() {
    }

    /**
     * The version selected for this TAP_SCHEMA.
     */
    public String getVersion() {
        return settings.getTapSchemaVersion();
    }

    public DataTypeMode getDataTypeMode() {
        return dataTypeMode;
    }

    private void loadSchemaKeysMetadata(String schemaName) throws SQLException {
        allKeys.addAll(getDBBroker(schemaName)
                .getKeys(this, schemaName, getRealSchemaName(schemaName)));
    public Set<Key> getAllKeys() {
        return allKeys;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Schema addChild(String schemaName) throws SQLException {
        LOG.debug("Adding schema {}", schemaName);

        Schema schema;

        if (!schemas.containsKey(schemaName)) {
            schema = null;
        } else {

            schema = schemas.get(schemaName);

            if (schema == null) {
                schema = new Schema(this, schemaName);
                schema.setStatus(Status.ADDED_NOT_PERSISTED);
                schemas.put(schemaName, schema);
                loadSchemaKeysMetadata(schemaName);
            } else {
                switch (schema.getStatus()) {
                    case TO_REMOVE:
                        schema.setStatus(Status.ADDED_PERSISTED);
                        break;
                    case REMOVED_NOT_PERSISTED:
                        schema.setStatus(Status.ADDED_NOT_PERSISTED);
                        break;
                    default:
                        throw new IllegalArgumentException("Cannot add the schema " + schemaName + ". Invalid status. Schema status is " + schema.getStatus());
                }
            }


        return schema;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeChild(String schemaName) {
        LOG.debug("Removing schema {}", schemaName);

        if (!schemas.containsKey(schemaName)) {
            throw new IllegalArgumentException("The database doesn't contains a schema named " + schemaName);
        }

        Schema schema = schemas.get(schemaName);
        if (schema == null || schema.getStatus() == Status.LOADED) {
            throw new IllegalArgumentException("Cannot remove the schema " + schemaName + ". It has never been added.");
        }

        if (schema.getStatus() == Status.ADDED_NOT_PERSISTED) {
            schema.setStatus(Status.REMOVED_NOT_PERSISTED);
        } else if (schema.getStatus() == Status.ADDED_PERSISTED) {
            schema.setStatus(Status.TO_REMOVE);
        }

        checkKeys();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Schema getChild(String childName, Status... statuses) {
        return TSMUtil.getChild(schemas, childName, statuses);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Schema> getChildren(Status... statuses) {
        return TSMUtil.getChildrenByStatus(schemas.values(), statuses);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> getAddableChildrenNames() {
        return TSMUtil.getAddableChildrenNames(schemas);
    }

    @Override
    public boolean isAddable(String childName) {
        return schemas.containsKey(childName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Schema> getAddedChildren() {
        return getChildren(Status.ADDED_PERSISTED, Status.ADDED_NOT_PERSISTED);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Schema> getAddedOrRemovedChildren() {
        return getChildren(Status.ADDED_PERSISTED, Status.ADDED_NOT_PERSISTED, Status.TO_REMOVE, Status.REMOVED_NOT_PERSISTED);
    }

    /**
     * This method has to be used after TAP_SCHEMA modifications are committed
     * to the database, in order to remove from the memory the schemas with
     * status {@code Status.TO_REMOVE} or {@code Status.REMOVED_NOT_PERSISTED}.
     */
    public void cleanSchema(String schemaName) {
        if (!schemas.containsKey(schemaName)) {
            throw new IllegalArgumentException("The TAP_SCHEMA doesn't contain the schema " + schemaName);
        }
        schemas.put(schemaName, null);
    }

    public SchemaModel getIvoaSchemaModel() {
        if (settings.isHasObscore()) {
            return SchemaModels.getIvoaSchemaModel(settings.getObscoreVersion());
    public void addEntireSchema(String schemaName) throws SQLException {
        Schema schema = addChild(schemaName);
        for (String tableName : schema.getAddableChildrenNames()) {
            Table table = schema.addChild(tableName);
            for (String columnName : table.getAddableChildrenNames()) {
                table.addChild(columnName);
            }
        }
    }

    /**
     * Save or update the TAP_SCHEMA changes into the database.
     */
    public void save() throws SQLException {

        DBBroker broker = getTapSchemaDBBroker();

        if (!exists) {
            SchemaModel tapSchemaModel = getTapSchemaModel();
            broker.createTapSchemaStructure(getRealName(), tapSchemaModel);
            // Adding TAP_SCHEMA into TAP_SCHEMA
            addEntireSchema(getName());
            fillColumnProperties(tapSchemaModel, getName());
                createAndAddIvoaSchema();
        // Clean inconsistency
        consistencyChecks = null;
    }

    public void createAndAddIvoaSchema() throws SQLException {
        SchemaModel ivoaSchemaModel = getIvoaSchemaModel();

        // ivoa schema has to be created into source database
        getSourceDBBroker().createIvoaSchemaStructure(ivoaSchemaModel);
        // Initializing ivoa schema slot in schemata maps
        schemas.put(ivoaSchemaModel.getName(), null);

        // Add ivoa schema into TAP_SCHEMA
        addEntireSchema(ivoaSchemaModel.getName());
        fillColumnsProperties(ivoaSchemaModel);
    }

    /**
     * Retrieve the maximum key id from all the schemas that are added into the
     * TAP_SCHEMA.
     *
     * @return the maximum key, if it exists, zero otherwise.
     */
    private int getMaxKeyId() {
        int maxKeyId = 0;
        for (Key key : allKeys) {
            if (key.getId() != null) {
                int keyId = Integer.parseInt(key.getId());
                if (keyId > maxKeyId) {
                    maxKeyId = keyId;
                }
            }
        }
        return maxKeyId;
    }

    private void fillKeyIds() {

        List<Key> newKeys = new ArrayList<>();
        for (Key key : allKeys) {
                newKeys.add(key);
            }
        }

        int maxKeyId = getMaxKeyId();
        for (Key newKey : newKeys) {
            maxKeyId++;
            newKey.setId(maxKeyId + "");
        }
    }

    /**
     * Set keys visibility based on other entities visibility (a key is visible
     * if all schemas, tables and columns involved have
     * {@link Status} {@code ADDED_PERSISTED} or {@code ADDED_NOT_PERSISTED}).
     */
    public final void checkKeys() {
        if (!loading) {

            for (Key key : allKeys) {

                // Check if key should be exposed in TAP_SCHEMA
                boolean keyVisible = true;

                for (KeyColumn keyColumn : key.getKeyColumns()) {
                    String schemaName = keyColumn.getParent().getFromSchemaName();
                    String tableName = keyColumn.getParent().getFromTableSimpleName();
                    String columnName = keyColumn.getFromColumn();
                    if (!isColumnVisible(schemaName, tableName, columnName)) {
                    schemaName = keyColumn.getParent().getTargetSchemaName();
                    tableName = keyColumn.getParent().getTargetTableSimpleName();
                    columnName = keyColumn.getTargetColumn();
                    if (!isColumnVisible(schemaName, tableName, columnName)) {
                        keyVisible = false;
                        break;
                    }
                }

                // TODO: use status instead of set visibile [?]
                key.setVisible(keyVisible);
            }
        }
    }

    /**
     * Print all TAP_SCHEMA tree (useful for debugging).
     */
    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder("\n");

        sb.append(String.format(">> TAP_SCHEMA %s <<\n", getName()));
        for (Schema schema : getChildren()) {
            sb.append("--");
            sb.append(schema.getName());
            sb.append(String.format(" [%s]", schema.getStatus()));
            sb.append("\n");
            List<Table> tables = schema.getChildren();
            for (int i = 0; i < tables.size(); i++) {
                Table table = tables.get(i);
                sb.append("   |--");
                sb.append(table.getName());
                sb.append(String.format(" [%s]", table.getStatus()));
                sb.append("\n");

                String padder = i < tables.size() - 1 ? "   |   " : "       ";

                for (Column column : table.getChildren()) {
                    sb.append(padder);
                    sb.append("|--");
                    sb.append(column.getName());
                    sb.append(String.format(" [%s]", column.getStatus()));
                    sb.append("\n");
                }

                sb.append("\n");
            }
        }

        sb.append("** Keys **\n");
        for (Key key : getVisibileKeys()) {
            sb.append(key);
            sb.append("\n");
        }

        return sb.toString();
    }

    public boolean isSchemaVisible(String schemaName) {
        Schema schema = schemas.get(schemaName);
        if (schema == null) {
            return false;
        }
        return schema.getStatus() == Status.ADDED_PERSISTED
                || schema.getStatus() == Status.ADDED_NOT_PERSISTED;
    }

    public boolean isTableVisible(String schemaName, String tableName) {
        if (!isSchemaVisible(schemaName)) {
            return false;
        }

        Table table = schemas.get(schemaName).getChild(tableName);
        if (table == null) {
            return false;
        }

        if (table.getStatus() == Status.ADDED_PERSISTED
                || table.getStatus() == Status.ADDED_NOT_PERSISTED) {
            return isSchemaVisible(schemaName);
        }

        return false;
    }

    public boolean isColumnVisible(String schemaName, String tableName, String columnName) {
        if (!isTableVisible(schemaName, tableName)) {
            return false;
        }

        Column column = schemas.get(schemaName).getChild(tableName).getChild(columnName);
        if (column == null) {
            return false;
        }

        if (column.getStatus() == Status.ADDED_PERSISTED
                || column.getStatus() == Status.ADDED_NOT_PERSISTED) {
            return isTableVisible(schemaName, tableName);
        }

        return false;
    }

    /**
     * Define if the TAP_SCHEMA schema was already written into the database.
     */
    public boolean exists() {
        return exists;
    }

    public ConsistencyChecks getConsistencyChecks() {
        return consistencyChecks;
    }

    public final SchemaModel getTapSchemaModel() {
        return SchemaModels.getTapSchemaModel(getVersion());
    public final TableModel getTableModel(String tableName) {
        return getTapSchemaModel().getTable(tableName);
    }

    public Map<String, Object> getSchemaMetadata(String schemaName) {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put(Schema.SCHEMA_NAME_KEY, schemaName);
        String dbNameMetadata = null;
        if (dbName != null && schemaName.equals(STANDARD_NAME)) {
            dbNameMetadata = dbName;
        }
        metadata.put(Schema.DBNAME, dbNameMetadata);
        return metadata;
    }

    public List<Key> getVisibileKeys() {
        List<Key> visibleKeys = new ArrayList<>();
        for (Key key : allKeys) {
            if (key.isVisible()) {
                visibleKeys.add(key);
            }
        }
        return visibleKeys;
    }
    private Integer getIntAsBool(Boolean value) {
        if (value == null) {
            return null;
        }
        return value ? 1 : 0;
    }

    public boolean isHasObscore() {
     * Fill descriptions of the TAP_SCHEMA schema entities for a given
     * SchemaModel (TAP_SCHEMA or ivoa).
    private void fillColumnProperties(SchemaModel schemaModel, String schemaName) {

        // check only on std, but valid also for principal (it depends on TS version)
        boolean useIntegerAsBool = getTapSchemaModel().getTable(COLUMNS_TABLE).get(Column.STD_KEY).getJavaType() == Integer.class;
        Schema schema = getChild(schemaName);
        schema.setValue(DESCRIPTION_KEY, schemaModel.getDescription());
        for (TableModel tableModel : schemaModel.getTables()) {
            Table table = schema.getChild(tableModel.getName());
            table.setValue(DESCRIPTION_KEY, tableModel.getDescription());
            for (ColumnModel columnModel : tableModel.getColumns()) {
                Column column = table.getChild(columnModel.getName());
                if (!columnModel.isMandatory() && column == null) {
                    // column could be null if it is not mandatory
                    continue;
                }
                column.setValue(DESCRIPTION_KEY, columnModel.getDescription());
                column.setValue(Column.UCD_KEY, columnModel.getUcd());
                column.setValue(Column.UNIT_KEY, columnModel.getUnit());
                column.setValue(Column.UTYPE_KEY, columnModel.getUtype());

                Object compatibleStd = useIntegerAsBool ? getIntAsBool(columnModel.isStandard()) : columnModel.isStandard();
                Object compatiblePrincipal = useIntegerAsBool ? getIntAsBool(columnModel.isPrincipal()) : columnModel.isPrincipal();
                column.setValue(Column.STD_KEY, compatibleStd);
                column.setValue(Column.PRINCIPAL_KEY, compatiblePrincipal);
    public void fillColumnsProperties(SchemaModel schemaModel) {
        fillColumnProperties(schemaModel, schemaModel.getName());

    /**
     * Allows to name the TAP_SCHEMA using the standard name, but referring to a
     * differently named schema. This value is null if the TAP_SCHEMA version
     * doesn't support the dbname column or if the schema name is already the
     * standard value.
     */
    public String getDBName() {
        return dbName;
    }

    public String getRealName() {
        return getRealSchemaName(getName());
    }

    public String getRealSchemaName(String schemaName) {
        if (dbName != null && STANDARD_NAME.equals(schemaName)) {
            return dbName;
        }
        return schemaName;
    }