/**
* OrbisGIS is a java GIS application dedicated to research in GIScience.
* OrbisGIS is developed by the GIS group of the DECIDE team of the
* Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
*
* The GIS group of the DECIDE team is located at :
*
* Laboratoire Lab-STICC – CNRS UMR 6285
* Equipe DECIDE
* UNIVERSITÉ DE BRETAGNE-SUD
* Institut Universitaire de Technologie de Vannes
* 8, Rue Montaigne - BP 561 56017 Vannes Cedex
*
* OrbisGIS is distributed under GPL 3 license.
*
* Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
* Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285)
*
* This file is part of OrbisGIS.
*
* OrbisGIS is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* OrbisGIS 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
* OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
*
* For more information, please consult: <http://www.orbisgis.org/>
* or contact directly:
* info_at_ orbisgis.org
*/
package org.orbisgis.geocatalogtree.impl.nodes;
import org.h2gis.utilities.JDBCUtilities;
import org.h2gis.utilities.SFSUtilities;
import org.h2gis.utilities.TableLocation;
import org.jooq.Catalog;
import org.jooq.DataType;
import org.jooq.Field;
import org.jooq.Meta;
import org.jooq.QueryPart;
import org.jooq.Schema;
import org.jooq.Table;
import org.jooq.TableField;
import org.jooq.UniqueKey;
import org.jooq.impl.DSL;
import org.orbisgis.corejdbc.DataManager;
import org.orbisgis.editorjdbc.TransferableSource;
import org.orbisgis.geocatalogtree.api.GeoCatalogTreeNode;
import org.orbisgis.geocatalogtree.api.GeoCatalogTreeNodeImpl;
import org.orbisgis.geocatalogtree.api.TreeNodeFactory;
import org.orbisgis.geocatalogtree.icons.GeocatalogIcon;
import org.orbisgis.sif.components.resourceTree.TreeSelectionIterable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
import javax.swing.ImageIcon;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.orbisgis.geocatalogtree.api.GeoCatalogTreeNode.*;
/**
* Create and update nodes
* @author Nicolas Fortin
*/
public class TreeNodeFactoryImpl implements TreeNodeFactory {
private static Logger LOGGER = LoggerFactory.getLogger(TreeNodeFactoryImpl.class);
private static I18n I18N = I18nFactory.getI18n(TreeNodeFactoryImpl.class);
private DataManager dataManager;
private boolean isH2 = false;
private String defaultSchema = "PUBLIC";
public TreeNodeFactoryImpl(DataManager dataManager) {
this.dataManager = dataManager;
try(Connection connection = dataManager.getDataSource().getConnection()) {
isH2 = JDBCUtilities.isH2DataBase(connection.getMetaData());
try {
if (connection.getSchema() != null) {
defaultSchema = connection.getSchema();
}
} catch (AbstractMethodError | Exception ex) {
// Driver has been compiled with JAVA 6, or is not implemented
}
} catch (SQLException ex) {
// Ignore
}
}
@Override
public String[] getParentNodeType() {
return new String[]{NODE_DATABASE, NODE_CATALOG, NODE_SCHEMA, NODE_TABLE, NODE_COLUMNS, NODE_INDEXES};
}
@Override
public void updateChildren(GeoCatalogTreeNode parent, Connection connection, JTree jTree) throws SQLException {
updateChildren(parent, getJooqQueryPart(connection,null, parent),connection, jTree);
}
public QueryPart getJooqQueryPart(Connection connection,QueryPart parentQueryPart, GeoCatalogTreeNode treeNode) {
if(treeNode == null) {
LOGGER.error("Cant find root catalog");
return null;
}
if(parentQueryPart == null) {
if(treeNode.getNodeType().equals(NODE_CATALOG)) {
return getCatalog(DSL.using(connection).meta(), treeNode.getNodeIdentifier());
} else {
parentQueryPart = getJooqQueryPart(connection, null, treeNode.getParent());
}
}
if(parentQueryPart != null) {
switch (treeNode.getNodeType()) {
case NODE_SCHEMA:
return ((Catalog) parentQueryPart).getSchema(treeNode.getNodeIdentifier());
case NODE_TABLE:
return ((Schema) parentQueryPart).getTable(TableLocation.parse(treeNode.getNodeIdentifier()).getTable());
case NODE_COLUMNS:
return parentQueryPart;
case NODE_INDEXES:
return parentQueryPart;
}
}
return null;
}
/**
* Load nodes. Can be extended by overriding this method.
* @param parent Parent node to fill or update
* @param parentQueryPart Parent node JOOQ instance
* @param connection Active connection
* @param jTree JTree that will receive items
* @param allNodes Nodes created
* @param allNodesQueryPart QueryPart of nodes created
*/
protected void loadNodes(GeoCatalogTreeNode parent, QueryPart parentQueryPart, Connection connection, JTree
jTree, List<GeoCatalogTreeNodeImpl> allNodes, List<QueryPart> allNodesQueryPart) throws SQLException {
DefaultTreeModel treeModel = (DefaultTreeModel)jTree.getModel();
// Read Meta and create all nodes
switch (parent.getNodeType()) {
case NODE_DATABASE:
loadCatalog(parent, DSL.using(connection).meta(), treeModel,allNodes, allNodesQueryPart);
break;
case NODE_CATALOG:
loadSchema((Catalog)parentQueryPart,allNodes, allNodesQueryPart);
break;
case NODE_SCHEMA:
loadTable((Schema)parentQueryPart,allNodes, allNodesQueryPart, connection);
break;
case NODE_TABLE:
allNodes.add(new GeoCatalogTreeNodeImpl(this, NODE_COLUMNS, I18N.tr("Columns"),
GeocatalogIcon.getIcon("column"), GeocatalogIcon.getIcon("column")).setSortChildren(false));
allNodesQueryPart.add(parentQueryPart);
allNodes.add(new GeoCatalogTreeNodeImpl(this, NODE_INDEXES, I18N.tr("Index"),
GeocatalogIcon.getIcon("index_folder"),GeocatalogIcon.getIcon("index_folder")));
allNodesQueryPart.add(parentQueryPart);
break;
case NODE_COLUMNS:
loadFields((Table)parentQueryPart, allNodes, allNodesQueryPart, connection);
break;
case NODE_INDEXES:
loadIndexes((Table)parentQueryPart, allNodes, allNodesQueryPart, connection);
break;
}
}
/**
* Load sub-nodes
* @param parent Parent node to fill or update
* @param parentQueryPart Parent node JOOQ instance
* @param connection Active connection
* @param jTree JTree that will receive items
* @throws SQLException
*/
public void updateChildren(GeoCatalogTreeNode parent, QueryPart parentQueryPart,Connection connection, JTree jTree) throws SQLException {
DefaultTreeModel treeModel = (DefaultTreeModel)jTree.getModel();
if(parent.getNodeType().isEmpty()) {
loadDatabase(DSL.using(connection).meta(), treeModel);
return;
}
List<GeoCatalogTreeNodeImpl> allNodes = new ArrayList<>(parent.getChildCount());
List<QueryPart> allNodesQueryPart = new ArrayList<>(parent.getChildCount());
loadNodes(parent, parentQueryPart, connection, jTree, allNodes, allNodesQueryPart);
Map<String, GeoCatalogTreeNode> oldNodes = parent.getChildrenIdentifier();
List<MutableTreeNode> nodeToInsert = new ArrayList<>(allNodes.size());
if(parent.isChildrenSorted()) {
// Check if the node already exist in the tree or if old nodes has been removed from db
for (int nodeId = 0; nodeId < allNodes.size(); nodeId++) {
GeoCatalogTreeNodeImpl node = allNodes.get(nodeId);
QueryPart nodeQueryPart = allNodesQueryPart.get(nodeId);
GeoCatalogTreeNode existingNode = oldNodes.get(node.getNodeIdentifier());
if (existingNode != null) {
// Node already exists
oldNodes.remove(node.getNodeIdentifier());
// Update the node if it contains at least one child
TreePath existingNodePath = new TreePath(((DefaultTreeModel) jTree.getModel()).getPathToRoot(existingNode));
if (existingNode.getChildCount() > 0 || jTree.isExpanded(existingNodePath)) {
TreeNodeFactory treeNodeFactory = existingNode.getFactory();
if (treeNodeFactory instanceof TreeNodeFactoryImpl) {
TreeNodeFactoryImpl childFactory = (TreeNodeFactoryImpl) treeNodeFactory;
childFactory.updateChildren(existingNode, nodeQueryPart, connection, jTree);
} else if (treeNodeFactory != null) {
treeNodeFactory.updateChildren(node, connection, jTree);
}
}
} else {
// New node, add it in the tree
nodeToInsert.add(node);
}
}
} else {
// In order to keep the parsed order we have to remove all old node (loosing tree states also)
nodeToInsert.addAll(allNodes);
}
// Add remove nodes on model using Swing event thread
List<MutableTreeNode> nodesToRemove = new ArrayList<>(oldNodes.isEmpty() ? 1 : oldNodes.size());
if(!oldNodes.isEmpty()) {
// Removed nodes
for(GeoCatalogTreeNode node : oldNodes.values()) {
if(isNodeMadeByThis(node)) {
nodesToRemove.add(node);
}
}
}
SwingUtilities.invokeLater(new TreeModelOperation(treeModel, nodeToInsert,nodesToRemove, parent));
}
protected boolean isNodeMadeByThis(GeoCatalogTreeNode node) {
return node.getFactory() == this;
}
/**
* @param meta JOOQ meta
* @param catalogName Catalog name
* @return Catalog or null
*/
public Catalog getCatalog(Meta meta, String catalogName) {
for(Catalog catalog : meta.getCatalogs()) {
if(catalogName.equals(catalog.getName())) {
return catalog;
}
}
return null;
}
/**
* Load root node or catalog as root if only one catalog in db
* @param meta JOOQ meta
* @param treeModel JTree model
*/
public void loadDatabase(Meta meta, DefaultTreeModel treeModel) {
List<Catalog> catalogs = meta.getCatalogs();
if(catalogs.size() == 1) {
loadCatalog(null, meta, treeModel, new ArrayList<GeoCatalogTreeNodeImpl>(), new ArrayList<QueryPart>());
} else {
treeModel.setRoot(new GeoCatalogTreeNodeImpl(this, NODE_DATABASE, "root"));
}
}
private void loadCatalog(GeoCatalogTreeNode parent, Meta meta, DefaultTreeModel treeModel, List<GeoCatalogTreeNodeImpl> nodes, List<QueryPart> nodesQueryPart) {
if(parent == null) {
treeModel.setRoot(new GeoCatalogTreeNodeImpl(this, NODE_CATALOG, meta.getCatalogs().get(0).getName()));
} else {
for(Catalog catalog : meta.getCatalogs()) {
nodes.add(new GeoCatalogTreeNodeImpl(this, NODE_CATALOG, catalog.getName()));
nodesQueryPart.add(catalog);
}
}
}
private void loadSchema(Catalog catalog, List<GeoCatalogTreeNodeImpl> nodes, List<QueryPart> nodesQueryPart) {
if(catalog != null) {
for (Schema schema : catalog.getSchemas()) {
nodes.add(new GeoCatalogTreeNodeImpl(this, NODE_SCHEMA, schema.getName(),
GeocatalogIcon.getIcon("schema"), GeocatalogIcon.getIcon("schema")));
nodesQueryPart.add(schema);
}
}
}
private void loadTable(Schema schema, List<GeoCatalogTreeNodeImpl> nodes, List<QueryPart> nodesQueryPart, Connection connection) throws SQLException {
if(schema != null) {
for (Table table : schema.getTables()) {
// Check if the table is a geo table
TableLocation identifier = new TableLocation(null, schema
.getName(), table.getName());
boolean hasGeoField = !SFSUtilities.getGeometryFields(connection, identifier).isEmpty();
if (hasGeoField) {
nodes.add(new GeoCatalogTreeNodeImpl(this, NODE_TABLE, identifier.toString(isH2), GeocatalogIcon
.getIcon("geofile"), GeocatalogIcon.getIcon("geofile")).setLabel(table.getName()).set
(GeoCatalogTreeNode.PROP_SPATIAL_TABLE, true));
} else {
nodes.add(new GeoCatalogTreeNodeImpl(this, NODE_TABLE, identifier.toString(isH2), GeocatalogIcon
.getIcon("flatfile"), GeocatalogIcon.getIcon("flatfile")).setLabel(table.getName()).set
(GeoCatalogTreeNode.PROP_SPATIAL_TABLE, false));
}
nodesQueryPart.add(table);
}
}
}
private void loadFields(Table table, List<GeoCatalogTreeNodeImpl> nodes, List<QueryPart> nodesQueryPart, Connection connection) throws SQLException {
if(table != null) {
// Fetch PK for icon
Set<String> pkFieldNames = new HashSet<>();
UniqueKey pk = table.getPrimaryKey();
if(pk != null) {
List<TableField> fields = pk.getFields();
for(TableField field : fields) {
pkFieldNames.add(field.getName());
}
}
// Fetch geometry fields
Set<String> spatialFields = new HashSet<>(SFSUtilities.getGeometryFields(connection, new TableLocation
(table.getSchema().getName(), table.getName())));
for(Field field : table.fields()) {
GeoCatalogTreeNodeImpl fieldNode;
if(pkFieldNames.contains(field.getName())) {
fieldNode = new GeoCatalogTreeNodeImpl(this, NODE_COLUMN, field.getName(), GeocatalogIcon.getIcon("key"));
} else {
ImageIcon icon;
DataType dataType = field.getDataType();
if(dataType.isNumeric()) {
icon = GeocatalogIcon.getIcon("field_num");
} else if(dataType.getSQLType() == Types.BOOLEAN) {
icon = GeocatalogIcon.getIcon("field_bool");
} else if(dataType.isString()) {
icon = GeocatalogIcon.getIcon("field_text");
} else if(dataType.isDateTime()) {
icon = GeocatalogIcon.getIcon("field_date");
} else if(spatialFields.contains(field.getName())) {
icon = GeocatalogIcon.getIcon("field_geom");
} else{
icon = GeocatalogIcon.getIcon("column");
}
fieldNode = new GeoCatalogTreeNodeImpl(this, NODE_COLUMN, field.getName(), icon);
}
fieldNode.set(GeoCatalogTreeNode.PROP_COLUMN_SPATIAL, spatialFields.contains(field.getName()));
fieldNode.set(GeoCatalogTreeNode.PROP_COLUMN_TYPE_NAME, field.getDataType().getTypeName());
fieldNode.setAllowsChildren(false);
nodes.add(fieldNode);
nodesQueryPart.add(field);
}
}
}
private void loadIndexes(Table table, List<GeoCatalogTreeNodeImpl> nodes, List<QueryPart> nodesQueryPart,
Connection connection) throws SQLException {
if (table != null) {
// Fetch all index
TableLocation tableLocation = new TableLocation(table.getSchema().getName(), table.getName());
List<String> spatialFields = SFSUtilities.getGeometryFields(connection, tableLocation);
// Fetch
DatabaseMetaData databaseMetaData = connection.getMetaData();
try(ResultSet rs = databaseMetaData.getIndexInfo(tableLocation.getCatalog(), tableLocation.getSchema(), tableLocation.getTable(), false, true)) {
while(rs.next()) {
String indexName = rs.getString("INDEX_NAME");
String columnName = rs.getString("COLUMN_NAME");
if(indexName != null) {
StringBuilder label = new StringBuilder(indexName);
if(columnName != null) {
label.append(" (");
label.append(columnName);
label.append(")");
}
ImageIcon leafIcon;
if(spatialFields.contains(columnName)) {
leafIcon = GeocatalogIcon.getIcon("index_geo");
} else {
leafIcon = GeocatalogIcon.getIcon("index_alpha");
}
nodes.add(new GeoCatalogTreeNodeImpl(this, GeoCatalogTreeNode
.NODE_INDEX, new TableLocation(tableLocation.getSchema(), indexName).toString(isH2), leafIcon).setLabel(label.toString()));
nodesQueryPart.add(table);
}
}
}
}
}
@Override
public void nodeValueVetoableChange(GeoCatalogTreeNode node, String newValue) throws PropertyVetoException {
}
@Override
public void nodeValueChange(GeoCatalogTreeNode node, String oldValue, String newValue) {
}
private String getSimplifiedTableIdentifier(String tableIdentifier) {
TableLocation columnTableIdentifier = TableLocation.parse(tableIdentifier);
return new TableLocation(columnTableIdentifier.getCatalog(), columnTableIdentifier.getSchema()
.equalsIgnoreCase(defaultSchema) ? "" : columnTableIdentifier.getSchema(), columnTableIdentifier
.getTable()).toString(isH2);
}
@Override
public Transferable createTransferable(JTree dbTree) {
List<String> sources = new ArrayList<>(dbTree.getSelectionCount());
List<String> columns = new ArrayList<>(dbTree.getSelectionCount());
String columnTable = "";
Transferable transferable = null;
for(GeoCatalogTreeNode treeNode : new TreeSelectionIterable<>(dbTree.getSelectionPaths(), GeoCatalogTreeNode.class)) {
switch (treeNode.getNodeType()) {
case GeoCatalogTreeNode.NODE_TABLE:
sources.add(getSimplifiedTableIdentifier(treeNode.getNodeIdentifier()));
break;
case GeoCatalogTreeNode.NODE_COLUMN:
if(columnTable.isEmpty()) {
columnTable = treeNode.getParent().getParent().getNodeIdentifier();
}
if(columnTable.equals(treeNode.getParent().getParent().getNodeIdentifier())) {
columns.add(treeNode.getNodeIdentifier());
}
break;
}
}
if(!sources.isEmpty()) {
transferable = new TransferableSource(sources.toArray(new String[sources.size()]), dataManager);
} else if(!columns.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT ");
for(int i=0; i<columns.size();i++) {
if(i > 0) {
sb.append(", ");
}
sb.append(columns.get(i));
}
sb.append(" FROM ");
sb.append(getSimplifiedTableIdentifier(columnTable));
sb.append(";");
transferable = new StringSelection(sb.toString());
}
return transferable;
}
/**
* Insertion/Deletion of nodes into the model. Has to be done in the Swing Event thread to avoid gui problems.
*/
private static class TreeModelOperation implements Runnable {
private DefaultTreeModel defaultTreeModel;
private List<MutableTreeNode> newChildren;
private List<MutableTreeNode> nodesToRemove;
private GeoCatalogTreeNode parent;
public TreeModelOperation(DefaultTreeModel defaultTreeModel, List<MutableTreeNode> newChildren
,List<MutableTreeNode> nodesToRemove, GeoCatalogTreeNode parent) {
this.defaultTreeModel = defaultTreeModel;
this.newChildren = newChildren;
this.nodesToRemove = nodesToRemove;
this.parent = parent;
}
@Override
public void run() {
// First remove deprecated nodes
for(MutableTreeNode node : nodesToRemove) {
defaultTreeModel.removeNodeFromParent(node);
}
if(parent.isChildrenSorted()) {
// Insert alphabetically
List<String> modelNodes = new ArrayList<>(parent.getChildCount() + newChildren.size());
for (int nodeId = 0; nodeId < parent.getChildCount(); nodeId++) {
modelNodes.add(parent.getChildAt(nodeId).toString());
}
while (!newChildren.isEmpty()) {
MutableTreeNode nodeToInsert = newChildren.remove(0);
int index = Collections.binarySearch(modelNodes, nodeToInsert.toString());
index = index >= 0 ? index : (-(index) - 1);
defaultTreeModel.insertNodeInto(nodeToInsert, parent, index);
modelNodes.add(index, nodeToInsert.toString());
}
} else {
for(MutableTreeNode nodeToInsert : newChildren) {
defaultTreeModel.insertNodeInto(nodeToInsert, parent, parent.getChildCount());
}
}
}
}
}