All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jdplus.sql.desktop.plugin.jdbc.JdbcProviderBuddy Maven / Gradle / Ivy

There is a newer version: 3.3.0
Show newest version
/*
 * Copyright 2013 National Bank of Belgium
 *
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved
 * by the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and
 * limitations under the Licence.
 */
package jdplus.sql.desktop.plugin.jdbc;

import ec.util.completion.AutoCompletionSource;
import ec.util.completion.ExtAutoCompletionSource;
import ec.util.various.swing.FontAwesome;
import internal.sql.base.api.DefaultConnectionSource;
import internal.sql.desktop.plugin.DbIcon;
import jdplus.sql.base.api.ConnectionSource;
import jdplus.sql.base.api.jdbc.JdbcBean;
import jdplus.sql.base.api.jdbc.JdbcProvider;
import jdplus.sql.desktop.plugin.SqlColumnListCellRenderer;
import jdplus.sql.desktop.plugin.SqlProviderBuddy;
import jdplus.sql.desktop.plugin.SqlTableListCellRenderer;
import jdplus.toolkit.base.tsp.DataSource;
import jdplus.toolkit.desktop.plugin.TsManager;
import jdplus.toolkit.desktop.plugin.actions.Configurable;
import jdplus.toolkit.desktop.plugin.properties.NodePropertySetBuilder;
import jdplus.toolkit.desktop.plugin.tsproviders.DataSourceProviderBuddy;
import jdplus.toolkit.desktop.plugin.tsproviders.TsProviderProperties;
import jdplus.toolkit.desktop.plugin.util.SimpleHtmlCellRenderer;
import lombok.NonNull;
import nbbrd.design.DirectImpl;
import nbbrd.io.text.Formatter;
import nbbrd.io.text.Parser;
import nbbrd.service.ServiceProvider;
import nbbrd.sql.jdbc.SqlConnectionSupplier;
import org.netbeans.api.db.explorer.ConnectionManager;
import org.netbeans.api.db.explorer.DatabaseConnection;
import org.netbeans.api.db.explorer.DatabaseException;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileUtil;
import org.openide.nodes.PropertySupport;
import org.openide.nodes.Sheet;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;

import javax.swing.*;
import java.awt.*;
import java.beans.IntrospectionException;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static ec.util.chart.impl.TangoColorScheme.DARK_ORANGE;
import static ec.util.chart.impl.TangoColorScheme.DARK_SCARLET_RED;
import static ec.util.chart.swing.SwingColorSchemeSupport.rgbToColor;

/**
 * @author Philippe Charles
 */
@DirectImpl
@ServiceProvider(DataSourceProviderBuddy.class)
public final class JdbcProviderBuddy implements DataSourceProviderBuddy, Configurable {

    private static final String SOURCE = "JNDI-JDBC";

    private final jdplus.sql.base.api.ConnectionManager manager;
    private final AutoCompletionSource dbSource;
    private final ListCellRenderer dbRenderer;
    private final ListCellRenderer tableRenderer;
    private final ListCellRenderer columnRenderer;
    private final Image warningBadge;
    private final Image errorBadge;

    public JdbcProviderBuddy() {
        this.manager = new DbExplorerConnectionManager();
        this.dbSource = dbExplorerSource();
        this.dbRenderer = new SimpleHtmlCellRenderer<>((DatabaseConnection o) -> "" + o.getDisplayName() + " - " + o.getName() + "");
        this.tableRenderer = new SqlTableListCellRenderer();
        this.columnRenderer = new SqlColumnListCellRenderer();
        this.warningBadge = FontAwesome.FA_EXCLAMATION_TRIANGLE.getImage(rgbToColor(DARK_ORANGE), 8f);
        this.errorBadge = FontAwesome.FA_EXCLAMATION_CIRCLE.getImage(rgbToColor(DARK_SCARLET_RED), 8f);
        overrideDefaultConnectionSupplier();
    }

    private Optional getProvider() {
        return TsManager.get().getProvider(JdbcProvider.class, SOURCE);
    }

    // this overrides default connection supplier since we don't have JNDI in JavaSE
    private void overrideDefaultConnectionSupplier() {
        getProvider()
                .ifPresent(o -> o.setConnectionManager(manager));
    }

    @Override
    public String getProviderName() {
        return SOURCE;
    }

    @Override
    public Image getIconOrNull(int type, boolean opened) {
        return DbIcon.DATABASE.getImageIcon().getImage();
    }

    @Override
    public Image getIconOrNull(DataSource dataSource, int type, boolean opened) {
        Image image = getIconOrNull(type, opened);
        switch (getStatus(dataSource)) {
            case DISCONNECTED:
                return ImageUtilities.mergeImages(image, warningBadge, 8, 8);
            case MISSING:
                return ImageUtilities.mergeImages(image, errorBadge, 8, 8);
            default:
                return image;
        }
    }

    private DbConnStatus getStatus(DataSource dataSource) {
        return getProvider()
                .map(provider -> provider.decodeBean(dataSource))
                .map(bean -> DbConnStatus.lookupByDisplayName(bean.getDatabase()))
                .orElse(DbConnStatus.CONNECTED);
    }

    @Override
    public List getSheetOrNull(@NonNull DataSource dataSource) {
        List result = new ArrayList<>();
        NodePropertySetBuilder b = new NodePropertySetBuilder().name("Connection");

        getProvider()
                .map(provider -> provider.decodeBean(dataSource))
                .map(bean -> new DbConnStatusProperty(bean.getDatabase()))
                .ifPresent(b::add);

        result.add(b.build());
        return result;
    }

    @Override
    public List getSheetOfBeanOrNull(@NonNull Object bean) throws IntrospectionException {
        return bean instanceof JdbcBean ? createSheetSets((JdbcBean) bean) : null;
    }

    private List createSheetSets(JdbcBean bean) {
        NodePropertySetBuilder b = new NodePropertySetBuilder();
        return Arrays.asList(
                createSource(b, bean),
                createCube(b, bean),
                createParsing(b, bean),
                createCache(b, bean)
        );
    }

    @NbBundle.Messages({
            "bean.source.display=Source",
            "bean.source.description=",
            "bean.database.display=Data source name",
            "bean.database.description=Data structure describing the connection to the database.",
            "bean.table.display=Table name",
            "bean.table.description=The name of the table (or view) that contains observations.",})
    private Sheet.Set createSource(NodePropertySetBuilder b, JdbcBean bean) {
        b.reset("source")
                .display(Bundle.bean_source_display())
                .description(Bundle.bean_source_description());

        b.withAutoCompletion()
                .select("database", bean::getDatabase, bean::setDatabase)
                .source(dbSource)
                .cellRenderer(dbRenderer)
                .display(Bundle.bean_database_display())
                .description(Bundle.bean_database_description())
                .add();

        b.withAutoCompletion()
                .select("table", bean::getTable, bean::setTable)
                .source(SqlProviderBuddy.getTableSource(manager, bean::getDatabase, bean::getTable))
                .cellRenderer(tableRenderer)
                .display(Bundle.bean_table_display())
                .description(Bundle.bean_table_description())
                .add();

        return b.build();
    }

    @NbBundle.Messages({
            "bean.cube.display=Cube structure",
            "bean.cube.description=",})
    private Sheet.Set createCube(NodePropertySetBuilder b, JdbcBean bean) {
        b.reset("cube")
                .display(Bundle.bean_cube_display())
                .description(Bundle.bean_cube_description());

        TsProviderProperties.addTableAsCubeStructure(b, bean::getCube, bean::setCube,
                SqlProviderBuddy.getColumnSource(manager, bean::getDatabase, bean::getTable),
                columnRenderer
        );

        return b.build();
    }

    @NbBundle.Messages({
            "bean.parsing.display=Parsing",
            "bean.parsing.description=",})
    private Sheet.Set createParsing(NodePropertySetBuilder b, JdbcBean bean) {
        b.reset("parsing")
                .display(Bundle.bean_parsing_display())
                .description(Bundle.bean_parsing_description());

        TsProviderProperties.addTableAsCubeParsing(b, bean::getCube, bean::setCube);

        return b.build();
    }

    @NbBundle.Messages({
            "bean.cache.display=Cache",
            "bean.cache.description=Mechanism used to improve performance.",})
    private Sheet.Set createCache(NodePropertySetBuilder b, JdbcBean bean) {
        b.reset("cache")
                .display(Bundle.bean_cache_display())
                .description(Bundle.bean_cache_description());

        TsProviderProperties.addBulkCube(b, bean::getCache, bean::setCache);

        return b.build();
    }

    @Override
    public void configure() {
        openServiceTab();
    }

    private static void openServiceTab() {
        Action serviceTabAction = FileUtil.getConfigObject("Actions/Window/org-netbeans-core-ide-ServicesTabAction.instance", Action.class);
        if (serviceTabAction != null) {
            EventQueue.invokeLater(() -> serviceTabAction.actionPerformed(null));
        }
    }

    private enum DbConnStatus {

        CONNECTED,
        DISCONNECTED,
        MISSING;

        public static DbConnStatus lookupByDisplayName(String dbName) {
            return DbExplorerUtil.getConnectionByDisplayName(dbName)
                    .map(o -> DbExplorerUtil.isConnected(o) ? CONNECTED : DISCONNECTED)
                    .orElse(MISSING);
        }
    }

    private static AutoCompletionSource dbExplorerSource() {
        return ExtAutoCompletionSource
                .builder(JdbcProviderBuddy::getDatabaseConnections)
                .behavior(AutoCompletionSource.Behavior.ASYNC)
                .postProcessor(JdbcProviderBuddy::getDatabaseConnections)
                .valueToString(DatabaseConnection::getDisplayName)
                .build();
    }

    private static List getDatabaseConnections() {
        return Arrays.asList(ConnectionManager.getDefault().getConnections());
    }

    private static List getDatabaseConnections(List values, String term) {
        Predicate filter = ExtAutoCompletionSource.basicFilter(term);
        return values.stream()
                .filter(o -> filter.test(o.getName()) || filter.test(o.getDisplayName()))
                .sorted(Comparator.comparing(DatabaseConnection::getDisplayName))
                .collect(Collectors.toList());
    }

    private static final class DbExplorerConnectionManager implements jdplus.sql.base.api.ConnectionManager {

        private final SqlConnectionSupplier supplier = new DbExplorerConnectionSupplier();

        @Override
        public @NonNull String getId() {
            return "dbexplorer";
        }

        @Override
        public @NonNull ConnectionSource getSource(@NonNull String connectionString) {
            return new DefaultConnectionSource(supplier, connectionString);
        }
    }

    @NbBundle.Messages({
            "# {0} - dbName",
            "dbexplorer.missingConnection=Cannot find connection named ''{0}''",
            "# {0} - dbName",
            "dbexplorer.noConnection=Not connected to the database ''{0}''",
            "# {0} - dbName",
            "dbexplorer.failedConnection=Failed to connect to the database ''{0}''",
            "# {0} - dbName",
            "dbexplorer.requestConnection=A process request a connection to the database ''{0}''.\nDo you want to open the connection dialog?",
            "dbexplorer.skip=Don't ask again"
    })
    private static final class DbExplorerConnectionSupplier implements SqlConnectionSupplier {

        private final Properties map = new Properties();

        @Override
        public Connection getConnection(String dbName) throws SQLException {
            DatabaseConnection o = DbExplorerUtil.getConnectionByDisplayName(dbName)
                    .orElseThrow(() -> new SQLException(Bundle.dbexplorer_missingConnection(dbName)));
            return getJDBCConnection(o);
        }

        private FailSafeConnection getJDBCConnection(DatabaseConnection o) throws SQLException {
            Connection conn = o.getJDBCConnection();
            if (conn != null) {
                return new FailSafeConnection(conn, o.getSchema());
            }
            if (connect(o) || connectWithDialog(o)) {
                return new FailSafeConnection(o.getJDBCConnection(), o.getSchema());
            }
            throw new SQLException(Bundle.dbexplorer_noConnection(o.getDisplayName()));
        }

        private boolean connect(DatabaseConnection o) throws SQLException {
            // let's try to connect without opening any dialog
            // must be done outside EDT
            if (!SwingUtilities.isEventDispatchThread()) {
                try {
                    // currently, the manager returns false if user is empty
                    // this behavior prevents some connections but it might be fixed in further versions
                    return ConnectionManager.getDefault().connect(o);
                } catch (DatabaseException ex) {
                    throw new SQLException(Bundle.dbexplorer_failedConnection(o.getDisplayName()), ex);
                }
            }
            return false;
        }

        private boolean connectWithDialog(final DatabaseConnection o) {
            try {
                return execInEDT(() -> {
                    String dbName = o.getDisplayName();
                    if (!isSkip(dbName)) {
                        JCheckBox checkBox = new JCheckBox(Bundle.dbexplorer_skip(), false);
                        Object[] msg = {Bundle.dbexplorer_requestConnection(dbName), checkBox};
                        Object option = DialogDisplayer.getDefault().notify(new NotifyDescriptor.Confirmation(msg, NotifyDescriptor.YES_NO_OPTION));
                        setSkip(dbName, checkBox.isSelected());
                        if (option == NotifyDescriptor.YES_OPTION) {
                            ConnectionManager.getDefault().showConnectionDialog(o);
                            return o.getJDBCConnection() != null;
                        }
                    }
                    return false;
                });
            } catch (Exception ex) {
                Exceptions.printStackTrace(ex);
            }
            return false;
        }

        boolean isSkip(String dbName) {
            return Parser.onBoolean().parse(map.getProperty(dbName, Boolean.FALSE.toString()));
        }

        void setSkip(String dbName, boolean remember) {
            map.setProperty(dbName, Formatter.onBoolean().formatAsString(remember));
        }
    }

    private static  T execInEDT(Callable callable) throws Exception {
        if (!SwingUtilities.isEventDispatchThread()) {
            RunnableFuture x = new FutureTask(callable);
            SwingUtilities.invokeAndWait(x);
            return x.get();
        } else {
            return callable.call();
        }
    }

    private static final class FailSafeConnection implements Connection {

        @lombok.experimental.Delegate
        private final Connection delegate;
        private final String defaultSchema;

        public FailSafeConnection(@NonNull Connection delegate, String defaultSchema) {
            this.delegate = delegate;
            this.defaultSchema = defaultSchema;
        }

        @Override
        public String getSchema() throws SQLException {
            try {
                String result = delegate.getSchema();
                return result != null ? result : defaultSchema;
            } catch (SQLException | AbstractMethodError ex) {
                // occurs when :
                // - method is not yet implemented
                // - driver follows specs older than JDBC4 specs
                return defaultSchema;
            }
        }

        @Override
        public void close() throws SQLException {
            // Calling Connection#close() is forbidden
            // See DatabaseConnection#getJDBCConnection()
        }
    }

    private static final class DbConnStatusProperty extends PropertySupport.ReadOnly {

        private final String dbName;

        public DbConnStatusProperty(String dbName) {
            super("Status", String.class, null, null);
            this.dbName = dbName;
        }

        @Override
        public String getValue() throws IllegalAccessException, InvocationTargetException {
            return DbConnStatus.lookupByDisplayName(dbName).name();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy