/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Olivier Grisel
* Florent Guillaume
*/
package org.nuxeo.ecm.directory.sql;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.nuxeo.common.xmap.annotation.XNode;
import org.nuxeo.common.xmap.annotation.XObject;
import org.nuxeo.ecm.core.schema.types.SchemaImpl;
import org.nuxeo.ecm.core.schema.types.primitives.StringType;
import org.nuxeo.ecm.core.storage.sql.ColumnType;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Delete;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Insert;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table.IndexType;
import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
import org.nuxeo.ecm.directory.AbstractReference;
import org.nuxeo.ecm.directory.BaseDirectoryDescriptor;
import org.nuxeo.ecm.directory.Directory;
import org.nuxeo.ecm.directory.DirectoryCSVLoader;
import org.nuxeo.ecm.directory.DirectoryException;
@XObject(value = "tableReference")
public class TableReference extends AbstractReference implements Cloneable {
@XNode("@field")
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
@Override
@XNode("@directory")
public void setTargetDirectoryName(String targetDirectoryName) {
this.targetDirectoryName = targetDirectoryName;
}
@XNode("@table")
protected String tableName;
@XNode("@sourceColumn")
protected String sourceColumn;
@XNode("@targetColumn")
protected String targetColumn;
@XNode("@dataFile")
protected String dataFileName;
private Table table;
private Dialect dialect;
private boolean initialized = false;
private SQLDirectory getSQLSourceDirectory() throws DirectoryException {
Directory dir = getSourceDirectory();
return (SQLDirectory) dir;
}
private void initialize(SQLSession sqlSession) throws DirectoryException {
Connection connection = sqlSession.sqlConnection;
SQLDirectory directory = getSQLSourceDirectory();
Table table = getTable();
SQLHelper helper = new SQLHelper(connection, table, directory.getDescriptor().getCreateTablePolicy());
boolean loadData = helper.setupTable();
if (loadData && dataFileName != null) {
// fake schema for DirectoryCSVLoader.loadData
SchemaImpl schema = new SchemaImpl(tableName, null);
schema.addField(sourceColumn, StringType.INSTANCE, null, 0, Collections.emptySet());
schema.addField(targetColumn, StringType.INSTANCE, null, 0, Collections.emptySet());
Insert insert = new Insert(table);
for (Column column : table.getColumns()) {
insert.addColumn(column);
}
try (PreparedStatement ps = connection.prepareStatement(insert.getStatement())) {
Consumer<Map<String, Object>> loader = new Consumer<Map<String, Object>>() {
@Override
public void accept(Map<String, Object> map) {
try {
ps.setString(1, (String) map.get(sourceColumn));
ps.setString(2, (String) map.get(targetColumn));
ps.execute();
} catch (SQLException e) {
throw new DirectoryException(e);
}
}
};
DirectoryCSVLoader.loadData(dataFileName, BaseDirectoryDescriptor.DEFAULT_DATA_FILE_CHARACTER_SEPARATOR,
schema, loader);
} catch (SQLException e) {
throw new DirectoryException(String.format("Table '%s' initialization failed", tableName), e);
}
}
}
@Override
public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException {
if (targetIds == null) {
return;
}
try (SQLSession session = getSQLSession()) {
addLinks(sourceId, targetIds, session);
}
}
@Override
public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException {
if (sourceIds == null) {
return;
}
try (SQLSession session = getSQLSession()) {
addLinks(sourceIds, targetId, session);
}
}
public void addLinks(String sourceId, List<String> targetIds, SQLSession session) throws DirectoryException {
if (targetIds == null) {
return;
}
for (String targetId : targetIds) {
addLink(sourceId, targetId, session, true);
}
}
public void addLinks(List<String> sourceIds, String targetId, SQLSession session) throws DirectoryException {
if (sourceIds == null) {
return;
}
for (String sourceId : sourceIds) {
addLink(sourceId, targetId, session, true);
}
}
public boolean exists(String sourceId, String targetId, SQLSession session) throws DirectoryException {
// String selectSql = String.format(
// "SELECT COUNT(*) FROM %s WHERE %s = ? AND %s = ?", tableName,
// sourceColumn, targetColumn);
Table table = getTable();
Select select = new Select(table);
select.setFrom(table.getQuotedName());
select.setWhat("count(*)");
String whereString = String.format("%s = ? and %s = ?", table.getColumn(sourceColumn).getQuotedName(),
table.getColumn(targetColumn).getQuotedName());
select.setWhere(whereString);
String selectSql = select.getStatement();
if (session.logger.isLogEnabled()) {
session.logger.logSQL(selectSql, Arrays.<Serializable> asList(sourceId, targetId));
}
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = session.sqlConnection.prepareStatement(selectSql);
ps.setString(1, sourceId);
ps.setString(2, targetId);
rs = ps.executeQuery();
rs.next();
return rs.getInt(1) > 0;
} catch (SQLException e) {
throw new DirectoryException(String.format("error reading link from %s to %s", sourceId, targetId), e);
} finally {
try {
if (rs != null) {
rs.close();
}
if (ps != null) {
ps.close();
}
} catch (SQLException sqle) {
throw new DirectoryException(sqle);
}
}
}
public void addLink(String sourceId, String targetId, SQLSession session, boolean checkExisting)
throws DirectoryException {
// OG: the following query should have avoided the round trips but
// does not work for some reason that might be related to a bug in the
// JDBC driver:
//
// String sql = String.format(
// "INSERT INTO %s (%s, %s) (SELECT ?, ? FROM %s WHERE %s = ? AND %s =
// ? HAVING COUNT(*) = 0)", tableName, sourceColumn, targetColumn,
// tableName, sourceColumn, targetColumn);
// first step: check that this link does not exist yet
if (checkExisting && exists(sourceId, targetId, session)) {
return;
}
// second step: add the link
// String insertSql = String.format(
// "INSERT INTO %s (%s, %s) VALUES (?, ?)", tableName,
// sourceColumn, targetColumn);
Table table = getTable();
Insert insert = new Insert(table);
insert.addColumn(table.getColumn(sourceColumn));
insert.addColumn(table.getColumn(targetColumn));
String insertSql = insert.getStatement();
if (session.logger.isLogEnabled()) {
session.logger.logSQL(insertSql, Arrays.<Serializable> asList(sourceId, targetId));
}
PreparedStatement ps = null;
try {
ps = session.sqlConnection.prepareStatement(insertSql);
ps.setString(1, sourceId);
ps.setString(2, targetId);
ps.execute();
} catch (SQLException e) {
throw new DirectoryException(String.format("error adding link from %s to %s", sourceId, targetId), e);
} finally {
try {
if (ps != null) {
ps.close();
}
} catch (SQLException sqle) {
throw new DirectoryException(sqle);
}
}
}
protected List<String> getIdsFor(String valueColumn, String filterColumn, String filterValue)
throws DirectoryException {
try (SQLSession session = getSQLSession()) {
// String sql = String.format("SELECT %s FROM %s WHERE %s = ?",
// table.getColumn(valueColumn), tableName, filterColumn);
Table table = getTable();
Select select = new Select(table);
select.setWhat(table.getColumn(valueColumn).getQuotedName());
select.setFrom(table.getQuotedName());
select.setWhere(table.getColumn(filterColumn).getQuotedName() + " = ?");
String sql = select.getStatement();
if (session.logger.isLogEnabled()) {
session.logger.logSQL(sql, Collections.<Serializable> singleton(filterValue));
}
List<String> ids = new LinkedList<String>();
try (PreparedStatement ps = session.sqlConnection.prepareStatement(sql)) {
ps.setString(1, filterValue);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
ids.add(rs.getString(valueColumn));
}
return ids;
}
} catch (SQLException e) {
throw new DirectoryException("error fetching reference values: ", e);
}
}
}
@Override
public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException {
return getIdsFor(sourceColumn, targetColumn, targetId);
}
@Override
public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException {
return getIdsFor(targetColumn, sourceColumn, sourceId);
}
public void removeLinksFor(String column, String entryId, SQLSession session) throws DirectoryException {
Table table = getTable();
String sql = String.format("DELETE FROM %s WHERE %s = ?", table.getQuotedName(), table.getColumn(column)
.getQuotedName());
if (session.logger.isLogEnabled()) {
session.logger.logSQL(sql, Collections.<Serializable> singleton(entryId));
}
PreparedStatement ps = null;
try {
ps = session.sqlConnection.prepareStatement(sql);
ps.setString(1, entryId);
ps.execute();
} catch (SQLException e) {
throw new DirectoryException("error remove links to " + entryId, e);
} finally {
try {
if (ps != null) {
ps.close();
}
} catch (SQLException sqle) {
throw new DirectoryException(sqle);
}
}
}
public void removeLinksForSource(String sourceId, SQLSession session) throws DirectoryException {
maybeInitialize(session);
removeLinksFor(sourceColumn, sourceId, session);
}
public void removeLinksForTarget(String targetId, SQLSession session) throws DirectoryException {
maybeInitialize(session);
removeLinksFor(targetColumn, targetId, session);
}
@Override
public void removeLinksForSource(String sourceId) throws DirectoryException {
try (SQLSession session = getSQLSession()) {
removeLinksForSource(sourceId, session);
}
}
@Override
public void removeLinksForTarget(String targetId) throws DirectoryException {
try (SQLSession session = getSQLSession()) {
removeLinksForTarget(targetId, session);
}
}
public void setIdsFor(String idsColumn, List<String> ids, String filterColumn, String filterValue,
SQLSession session) throws DirectoryException {
List<String> idsToDelete = new LinkedList<String>();
Set<String> idsToAdd = new HashSet<String>();
if (ids != null) { // ids may be null
idsToAdd.addAll(ids);
}
Table table = getTable();
// iterate over existing links to find what to add and what to remove
String selectSql = String.format("SELECT %s FROM %s WHERE %s = ?", table.getColumn(idsColumn).getQuotedName(),
table.getQuotedName(), table.getColumn(filterColumn).getQuotedName());
PreparedStatement ps = null;
try {
ps = session.sqlConnection.prepareStatement(selectSql);
ps.setString(1, filterValue);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String existingId = rs.getString(1);
if (idsToAdd.contains(existingId)) {
// to not add already existing ids
idsToAdd.remove(existingId);
} else {
// delete unwanted existing ids
idsToDelete.add(existingId);
}
}
rs.close();
} catch (SQLException e) {
throw new DirectoryException("failed to fetch existing links for " + filterValue, e);
} finally {
try {
if (ps != null) {
ps.close();
}
} catch (SQLException sqle) {
throw new DirectoryException(sqle);
}
}
if (!idsToDelete.isEmpty()) {
// remove unwanted links
// String deleteSql = String.format(
// "DELETE FROM %s WHERE %s = ? AND %s = ?", tableName,
// filterColumn, idsColumn);
Delete delete = new Delete(table);
String whereString = String.format("%s = ? AND %s = ?", table.getColumn(filterColumn).getQuotedName(),
table.getColumn(idsColumn).getQuotedName());
delete.setWhere(whereString);
String deleteSql = delete.getStatement();
try {
ps = session.sqlConnection.prepareStatement(deleteSql);
for (String unwantedId : idsToDelete) {
if (session.logger.isLogEnabled()) {
session.logger.logSQL(deleteSql, Arrays.<Serializable> asList(filterValue, unwantedId));
}
ps.setString(1, filterValue);
ps.setString(2, unwantedId);
ps.execute();
}
} catch (SQLException e) {
throw new DirectoryException("failed to remove unwanted links for " + filterValue, e);
} finally {
try {
if (ps != null) {
ps.close();
}
} catch (SQLException sqle) {
throw new DirectoryException(sqle);
}
}
}
if (!idsToAdd.isEmpty()) {
// add missing links
if (filterColumn.equals(sourceColumn)) {
for (String missingId : idsToAdd) {
addLink(filterValue, missingId, session, false);
}
} else {
for (String missingId : idsToAdd) {
addLink(missingId, filterValue, session, false);
}
}
}
}
public void setSourceIdsForTarget(String targetId, List<String> sourceIds, SQLSession session)
throws DirectoryException {
setIdsFor(sourceColumn, sourceIds, targetColumn, targetId, session);
}
public void setTargetIdsForSource(String sourceId, List<String> targetIds, SQLSession session)
throws DirectoryException {
setIdsFor(targetColumn, targetIds, sourceColumn, sourceId, session);
}
@Override
public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException {
try (SQLSession session = getSQLSession()) {
setSourceIdsForTarget(targetId, sourceIds, session);
}
}
@Override
public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException {
try (SQLSession session = getSQLSession()) {
setTargetIdsForSource(sourceId, targetIds, session);
}
}
// TODO add support for the ListDiff type
protected SQLSession getSQLSession() throws DirectoryException {
if (!initialized) {
try (SQLSession sqlSession = (SQLSession) getSourceDirectory().getSession()) {
initialize(sqlSession);
initialized = true;
}
}
return (SQLSession) getSourceDirectory().getSession();
}
/**
* Initialize if needed, using an existing session.
*
* @param sqlSession
* @throws DirectoryException
*/
protected void maybeInitialize(SQLSession sqlSession) throws DirectoryException {
if (!initialized) {
initialize(sqlSession);
initialized = true;
}
}
public Table getTable() throws DirectoryException {
if (table == null) {
boolean nativeCase = getSQLSourceDirectory().useNativeCase();
table = SQLHelper.addTable(tableName, getDialect(), nativeCase);
SQLHelper.addColumn(table, sourceColumn, ColumnType.STRING, nativeCase);
SQLHelper.addColumn(table, targetColumn, ColumnType.STRING, nativeCase);
// index added for Azure
table.addIndex(null, IndexType.MAIN_NON_PRIMARY, sourceColumn);
}
return table;
}
private Dialect getDialect() throws DirectoryException {
if (dialect == null) {
dialect = getSQLSourceDirectory().getDialect();
}
return dialect;
}
public String getSourceColumn() {
return sourceColumn;
}
public String getTargetColumn() {
return targetColumn;
}
public String getTargetDirectoryName() {
return targetDirectoryName;
}
public String getTableName() {
return tableName;
}
public String getDataFileName() {
return dataFileName;
}
/**
* @since 5.6
*/
@Override
public TableReference clone() {
TableReference clone = (TableReference) super.clone();
// basic fields are already copied by super.clone()
return clone;
}
}