rapture.kernel.StructuredApiImpl.java Source code

Java tutorial

Introduction

Here is the source code for rapture.kernel.StructuredApiImpl.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2011-2016 Incapture Technologies LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package rapture.kernel;

import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import rapture.common.CallingContext;
import rapture.common.ForeignKey;
import rapture.common.RaptureURI;
import rapture.common.Scheme;
import rapture.common.StoredProcedureParams;
import rapture.common.StoredProcedureResponse;
import rapture.common.StructuredRepoConfig;
import rapture.common.StructuredRepoConfigStorage;
import rapture.common.TableIndex;
import rapture.common.api.StructuredApi;
import rapture.common.exception.RaptureExceptionFactory;
import rapture.repo.StructuredRepo;
import rapture.structured.DefaultValidator;
import rapture.structured.Validator;

public class StructuredApiImpl extends KernelBase implements StructuredApi {

    private Validator validator;

    public StructuredApiImpl(Kernel raptureKernel) {
        super(raptureKernel);
        validator = new DefaultValidator();
    }

    @Override
    public void createStructuredRepo(CallingContext context, String repoURI, String config) {
        checkParameter("URI", repoURI);
        checkParameter("Config", config);

        RaptureURI internalURI = new RaptureURI(repoURI, Scheme.STRUCTURED);
        String authority = internalURI.getAuthority();
        if ((authority == null) || authority.isEmpty()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NoAuthority")); //$NON-NLS-1$
        }
        if (internalURI.hasDocPath()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NoDocPath", repoURI)); //$NON-NLS-1$
        }

        // TODO write a config validator

        if (structuredRepoExists(context, repoURI)) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("Exists", internalURI.toShortString())); //$NON-NLS-1$
        }

        StructuredRepoConfig structured = new StructuredRepoConfig();
        structured.setAuthority(authority);
        structured.setConfig(config);
        StructuredRepoConfigStorage.add(structured, context.getUser(), "Create Structured repository");
    }

    @Override
    public void deleteStructuredRepo(CallingContext context, String repoURI) {
        RaptureURI uri = new RaptureURI(repoURI, Scheme.STRUCTURED);
        if (uri.hasDocPath()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NoDocPath", repoURI)); //$NON-NLS-1$
        }
        getRepoOrFail(uri.getAuthority()).drop();
        removeRepoFromCache(uri.getAuthority());
        StructuredRepoConfigStorage.deleteByAddress(uri, context.getUser(), "Delete structured repo");
    }

    @Override
    public Boolean structuredRepoExists(CallingContext context, String repoURI) {
        RaptureURI uri = new RaptureURI(repoURI, Scheme.STRUCTURED);
        if (uri.hasDocPath()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NoDocPath", repoURI)); //$NON-NLS-1$
        }
        return getRepoFromCache(uri.getAuthority()) != null;
    }

    @Override
    public StructuredRepoConfig getStructuredRepoConfig(CallingContext ctx, String uriStr) {
        RaptureURI uri = new RaptureURI(uriStr, Scheme.STRUCTURED);
        if (uri.hasDocPath()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    "Structured repository URI " + uriStr + " can't have a doc path: '" + uri.getDocPath() + "'");
        }
        return StructuredRepoConfigStorage.readByAddress(uri);
    }

    @Override
    public List<StructuredRepoConfig> getStructuredRepoConfigs(CallingContext context) {
        return StructuredRepoConfigStorage.readAll();
    }

    private StructuredRepo getRepoOrFail(String authority) {
        StructuredRepo repo = getRepoFromCache(authority);
        if (repo == null) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NoSuchRepo", Scheme.STRUCTURED + "://" + authority)); //$NON-NLS-1$
        } else {
            return repo;
        }
    }

    private StructuredRepo getRepoFromCache(String authority) {
        return Kernel.getRepoCacheManager().getStructuredRepo(authority);
    }

    private void removeRepoFromCache(String authority) {
        Kernel.getRepoCacheManager().removeRepo(Scheme.STRUCTURED.toString(), authority);
    }

    @Override
    public void createTableUsingSql(CallingContext context, String schema, String rawSql) {
        StructuredRepo repo = getRepoOrFail(schema);
        registerWithTxManager(context, repo);
        try {
            repo.createTableUsingSql(context, rawSql);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void createTable(CallingContext context, String tableUri, Map<String, String> columns) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        // We do not allow tables with dots in them. Standard SQL does not allow this and we won't either.
        // The complexity involved to go back and forth and text-parse is not worth it.
        validateTable(uri.getDocPath());
        validateColumns(columns.keySet());
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.createTable(uri.getDocPath(), columns);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void dropTable(CallingContext context, String tableUri) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.dropTable(uri.getDocPath());
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public Boolean tableExists(CallingContext context, String tableUri) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        try {
            return getRepoOrFail(uri.getAuthority()).tableExists(uri.getDocPath());
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public List<String> getSchemas(CallingContext context) {
        List<StructuredRepoConfig> configs = getStructuredRepoConfigs(context);
        return Lists.transform(configs, new Function<StructuredRepoConfig, String>() {
            @Nullable
            @Override
            public String apply(StructuredRepoConfig structuredRepoConfig) {
                return structuredRepoConfig.getAuthority();
            }
        });
    }

    @Override
    public List<String> getTables(CallingContext context, String repoURI) {
        RaptureURI uri = new RaptureURI(repoURI, Scheme.STRUCTURED);
        if (uri.hasDocPath()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NoDocPath", repoURI)); //$NON-NLS-1$
        }
        return getRepoOrFail(uri.getAuthority()).getTables();
    }

    @Override
    public Map<String, String> describeTable(CallingContext context, String tableUri) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        return getRepoOrFail(uri.getAuthority()).describeTable(uri.getDocPath()).getRows();
    }

    @Override
    public void addTableColumns(CallingContext context, String tableUri, Map<String, String> columns) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        validateColumns(columns.keySet());
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.addTableColumns(uri.getDocPath(), columns);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void deleteTableColumns(CallingContext context, String tableUri, List<String> columnNames) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.deleteTableColumns(uri.getDocPath(), columnNames);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void updateTableColumns(CallingContext context, String tableUri, Map<String, String> columns) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        validateColumns(columns.keySet());
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.updateTableColumns(uri.getDocPath(), columns);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void renameTableColumns(CallingContext context, String tableUri, Map<String, String> columnNames) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.renameTableColumns(uri.getDocPath(), columnNames);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void createIndex(CallingContext context, String tableUri, String indexName, List<String> columnNames) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.createIndex(uri.getDocPath(), indexName, columnNames);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void dropIndex(CallingContext context, String tableUri, String indexName) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.dropIndex(indexName);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public List<TableIndex> getIndexes(CallingContext context, String tableUri) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        return repo.getIndexes(uri.getDocPath());
    }

    @Override
    public String getPrimaryKey(CallingContext context, String tableUri) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        return repo.getPrimaryKey(uri.getDocPath());
    }

    @Override
    public List<ForeignKey> getForeignKeys(CallingContext context, String tableUri) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        return repo.getForeignKeys(uri.getDocPath());
    }

    @Override
    public List<Map<String, Object>> selectJoinedRows(CallingContext context, List<String> tableUris,
            List<String> columnNames, String from, String where, List<String> order, Boolean ascending, int limit) {
        Pair<String, List<String>> pair = getAuthorityAndDocPaths(tableUris);
        return getRepoOrFail(pair.getLeft()).selectJoinedRows(pair.getRight(), columnNames, from, where, order,
                ascending, limit);
    }

    @Override
    public List<Map<String, Object>> selectUsingSql(CallingContext context, String schema, String rawSql) {
        return getRepoOrFail(schema).selectUsingSql(context, rawSql);
    }

    @Override
    public List<Map<String, Object>> selectRows(CallingContext context, String tableUri, List<String> columnNames,
            String where, List<String> order, Boolean ascending, int limit) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        return getRepoOrFail(uri.getAuthority()).selectRows(uri.getDocPath(), columnNames, where, order, ascending,
                limit);
    }

    @Override
    public void insertUsingSql(CallingContext context, String schema, String rawSql) {
        StructuredRepo repo = getRepoOrFail(schema);
        registerWithTxManager(context, repo);
        try {
            repo.insertUsingSql(context, rawSql);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void insertRow(CallingContext context, String tableUri, Map<String, Object> values) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.insertRow(uri.getDocPath(), values);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void insertRows(CallingContext context, String tableUri, List<Map<String, Object>> values) {
        // TODO: validate input, each map has same number of keys
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.insertRows(uri.getDocPath(), values);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void updateUsingSql(CallingContext context, String schema, String rawSql) {
        StructuredRepo repo = getRepoOrFail(schema);
        registerWithTxManager(context, repo);
        try {
            repo.updateUsingSql(context, rawSql);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void deleteUsingSql(CallingContext context, String schema, String rawSql) {
        StructuredRepo repo = getRepoOrFail(schema);
        registerWithTxManager(context, repo);
        try {
            repo.deleteUsingSql(context, rawSql);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void updateRows(CallingContext context, String tableUri, Map<String, Object> values, String where) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.updateRows(uri.getDocPath(), values, where);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void deleteRows(CallingContext context, String tableUri, String where) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.deleteRows(uri.getDocPath(), where);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    private void registerWithTxManager(CallingContext context, StructuredRepo repo) {
        String txId = getTxId(context);
        if (TransactionManager.isTransactionFailed(txId)) {
            throw RaptureExceptionFactory.create("Transaction " + txId + " already failed, abort now");
        }
        if (TransactionManager.isTransactionActive(txId)) {
            TransactionManager.registerThread(txId);
            TransactionManager.registerRepo(txId, repo);
        }
    }

    @Override
    public Boolean begin(CallingContext context) {
        return TransactionManager.begin(getTxId(context));
    }

    @Override
    public Boolean commit(CallingContext context) {
        return TransactionManager.commit(getTxId(context));
    }

    @Override
    public Boolean rollback(CallingContext context) {
        return TransactionManager.rollback(getTxId(context));
    }

    @Override
    public Boolean abort(CallingContext context, String transactionId) {
        return TransactionManager.rollback(transactionId);
    }

    @Override
    public List<String> getTransactions(CallingContext context) {
        return ImmutableList.copyOf(TransactionManager.getTransactions());
    }

    private String getTxId(CallingContext context) {
        return context.getContext();
    }

    private void validateTable(String docPath) {
        if (!validator.isTableValid(docPath)) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST, "Invalid table name provided");
        }
    }

    private void validateColumns(Set<String> columns) {
        for (String column : columns) {
            if (!validator.isColumnValid(column)) {
                throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                        "Invalid column name provided");
            }
        }
    }

    private void validateCursorCount(int count) {
        if (!validator.isCursorCountValid(count)) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    "Cursor count must be greater than 0");
        }
    }

    @Override
    public String getDdl(CallingContext context, String uriStr, Boolean includeTableData) {
        RaptureURI uri = new RaptureURI(uriStr, Scheme.STRUCTURED);
        return getRepoOrFail(uri.getAuthority()).getDdl(uri.getDocPath(), includeTableData);
    }

    @Override
    public String getCursorUsingSql(CallingContext context, String schema, String rawSql) {
        return getRepoOrFail(schema).getCursorUsingSql(context, rawSql);
    }

    @Override
    public String getCursor(CallingContext context, String tableUri, List<String> columnNames, String where,
            List<String> order, Boolean ascending, int limit) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        return getRepoOrFail(uri.getAuthority()).getCursor(uri.getDocPath(), columnNames, where, order, ascending,
                limit);
    }

    @Override
    public String getCursorForJoin(CallingContext context, List<String> tableUris, List<String> columnNames,
            String from, String where, List<String> order, Boolean ascending, int limit) {
        Pair<String, List<String>> pair = getAuthorityAndDocPaths(tableUris);
        return getRepoOrFail(pair.getLeft()).getCursorForJoin(pair.getRight(), columnNames, from, where, order,
                ascending, limit);
    }

    @Override
    public List<Map<String, Object>> next(CallingContext context, String tableUri, String cursorId, int count) {
        validateCursorCount(count);
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        return getRepoOrFail(uri.getAuthority()).next(uri.getDocPath(), cursorId, count);
    }

    ;

    @Override
    public List<Map<String, Object>> previous(CallingContext context, String tableUri, String cursorId, int count) {
        validateCursorCount(count);
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        return getRepoOrFail(uri.getAuthority()).previous(uri.getDocPath(), cursorId, count);
    }

    ;

    @Override
    public void closeCursor(CallingContext context, String tableUri, String cursorId) {
        RaptureURI uri = new RaptureURI(tableUri, Scheme.STRUCTURED);
        getRepoOrFail(uri.getAuthority()).closeCursor(uri.getDocPath(), cursorId);
    }

    ;

    @Override
    public void createProcedureCallUsingSql(CallingContext context, String procUri, String rawSql) {
        RaptureURI uri = new RaptureURI(procUri);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.createProcedureCallUsingSql(context, rawSql);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public StoredProcedureResponse callProcedure(CallingContext context, String procUri,
            StoredProcedureParams params) {
        RaptureURI uri = new RaptureURI(procUri);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        String procName = uri.getDocPath();
        registerWithTxManager(context, repo);
        try {
            return repo.callProcedure(context, procName, params);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    @Override
    public void dropProcedureUsingSql(CallingContext context, String procUri, String rawSql) {
        RaptureURI uri = new RaptureURI(procUri);
        StructuredRepo repo = getRepoOrFail(uri.getAuthority());
        registerWithTxManager(context, repo);
        try {
            repo.dropProcedureUsingSql(context, rawSql);
        } catch (Exception e) {
            TransactionManager.transactionFailed(getTxId(context));
            throw RaptureExceptionFactory.create(e.getMessage(), e.getCause());
        }
    }

    /**
     * Trusted method not exposed via the API. Mainly used for Feature Installer to re-install schemas and tables
     *
     * @param uriStr - uri representing the structured repo
     * @param ddl    - full SQL string with CREATE and INSERT statements
     */
    public void executeDdl(String uriStr, String ddl, boolean alter) {
        getRepoOrFail(new RaptureURI(uriStr, Scheme.STRUCTURED).getAuthority()).executeDdl(ddl, alter);
    }

    /**
     * Used in joins to get a single authority and the docpaths as a List
     *
     * @param tableUris
     * @return
     */
    private Pair<String, List<String>> getAuthorityAndDocPaths(List<String> tableUris) {
        if (CollectionUtils.isEmpty(tableUris)) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    apiMessageCatalog.getMessage("NullEmpty", "list of tableUris")); //$NON-NLS-1$ //$NON-NLS-2$
        }
        // we assume the join is across the same DB provider
        RaptureURI uri = new RaptureURI(tableUris.get(0), Scheme.STRUCTURED);
        List<String> docPaths = new ArrayList<>();
        for (String tableUri : tableUris) {
            RaptureURI ruri = new RaptureURI(tableUri, Scheme.STRUCTURED);
            docPaths.add(ruri.getDocPath());
        }
        return Pair.of(uri.getAuthority(), docPaths);
    }

}