/*
* (C) Copyright 2006-2016 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:
* George Lefter
* Florent Guillaume
*/
package org.nuxeo.ecm.directory.sql;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.transaction.Synchronization;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.cache.CacheService;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.core.schema.types.Schema;
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.Table;
import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
import org.nuxeo.ecm.directory.AbstractDirectory;
import org.nuxeo.ecm.directory.DirectoryCSVLoader;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.datasource.ConnectionHelper;
import org.nuxeo.runtime.transaction.TransactionHelper;
public class SQLDirectory extends AbstractDirectory {
protected class TxSessionCleaner implements Synchronization {
private final SQLSession session;
Throwable initContext = captureInitContext();
protected TxSessionCleaner(SQLSession session) {
this.session = session;
}
protected Throwable captureInitContext() {
if (!log.isDebugEnabled()) {
return null;
}
return new Throwable("SQL directory session init context in " + SQLDirectory.this);
}
protected void checkIsNotLive() {
try {
if (!session.isLive()) {
return;
}
if (initContext != null) {
log.warn("Closing a sql directory session for you " + session, initContext);
} else {
log.warn("Closing a sql directory session for you " + session);
}
if (!TransactionHelper.isTransactionActiveOrMarkedRollback()) {
log.warn("Closing sql directory session outside a transaction" + session);
}
session.close();
} catch (DirectoryException e) {
log.error("Cannot state on sql directory session before commit " + SQLDirectory.this, e);
}
}
@Override
public void beforeCompletion() {
checkIsNotLive();
}
@Override
public void afterCompletion(int status) {
checkIsNotLive();
}
}
public static final Log log = LogFactory.getLog(SQLDirectory.class);
public static final String TENANT_ID_FIELD = "tenantId";
private final boolean nativeCase;
private Table table;
private Schema schema;
private Map<String, Field> schemaFieldMap;
// columns to fetch when an entry is read (with the password)
protected List<Column> readColumnsAll;
// columns to fetch when an entry is read (excludes the password)
protected List<Column> readColumns;
// columns to fetch when an entry is read (with the password), as SQL
protected String readColumnsAllSQL;
// columns to fetch when an entry is read (excludes the password), as SQL
protected String readColumnsSQL;
private volatile Dialect dialect;
public SQLDirectory(SQLDirectoryDescriptor descriptor) {
super(descriptor);
nativeCase = Boolean.TRUE.equals(descriptor.nativeCase);
// register the references to other directories
addReferences(descriptor.getInverseReferences());
addReferences(descriptor.getTableReferences());
// cache parameterization
cache.setEntryCacheName(descriptor.cacheEntryName);
cache.setEntryCacheWithoutReferencesName(descriptor.cacheEntryWithoutReferencesName);
cache.setNegativeCaching(descriptor.negativeCaching);
// Cache fallback
CacheService cacheService = Framework.getLocalService(CacheService.class);
if (cacheService != null) {
if (descriptor.cacheEntryName == null && descriptor.getCacheMaxSize() != 0) {
cache.setEntryCacheName("cache-" + getName());
cacheService.registerCache("cache-" + getName(),
descriptor.getCacheMaxSize(),
descriptor.getCacheTimeout() / 60);
}
if (descriptor.cacheEntryWithoutReferencesName == null && descriptor.getCacheMaxSize() != 0) {
cache.setEntryCacheWithoutReferencesName(
"cacheWithoutReference-" + getName());
cacheService.registerCache("cacheWithoutReference-" + getName(),
descriptor.getCacheMaxSize(),
descriptor.getCacheTimeout() / 60);
}
}
}
@Override
public SQLDirectoryDescriptor getDescriptor() {
return (SQLDirectoryDescriptor) descriptor;
}
/**
* Lazily initializes the connection.
*
* @return {@code true} if CSV data should be loaded
* @since 8.4
*/
protected boolean initConnectionIfNeeded() {
// double checked locking with volatile pattern to ensure concurrent lazy init
if (dialect == null) {
synchronized (this) {
if (dialect == null) {
return initConnection();
}
}
}
return false;
}
/**
* Initializes the table.
*
* @return {@code true} if CSV data should be loaded
* @since 6.0
*/
protected boolean initConnection() {
SQLDirectoryDescriptor descriptor = getDescriptor();
try (Connection sqlConnection = getConnection()) {
dialect = Dialect.createDialect(sqlConnection, null);
// setup table and fields maps
String tableName = descriptor.tableName == null ? descriptor.name : descriptor.tableName;
table = SQLHelper.addTable(tableName, dialect, useNativeCase());
SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
schema = schemaManager.getSchema(getSchema());
if (schema == null) {
throw new DirectoryException("schema not found: " + getSchema());
}
schemaFieldMap = new LinkedHashMap<>();
readColumnsAll = new LinkedList<>();
readColumns = new LinkedList<>();
boolean hasPrimary = false;
for (Field f : schema.getFields()) {
String fieldName = f.getName().getLocalName();
schemaFieldMap.put(fieldName, f);
if (!isReference(fieldName)) {
boolean isId = fieldName.equals(getIdField());
ColumnType type = ColumnType.fromField(f);
if (isId && descriptor.isAutoincrementIdField()) {
type = ColumnType.AUTOINC;
}
Column column = SQLHelper.addColumn(table, fieldName, type, useNativeCase());
if (isId) {
if (descriptor.isAutoincrementIdField()) {
column.setIdentity(true);
}
column.setPrimary(true);
column.setNullable(false);
hasPrimary = true;
}
readColumnsAll.add(column);
if (!fieldName.equals(descriptor.passwordField)) {
readColumns.add(column);
}
}
}
readColumnsAllSQL = readColumnsAll.stream().map(Column::getQuotedName).collect(Collectors.joining(", "));
readColumnsSQL = readColumns.stream().map(Column::getQuotedName).collect(Collectors.joining(", "));
if (!hasPrimary) {
throw new DirectoryException(String.format(
"Directory '%s' id field '%s' is not present in schema '%s'", getName(), getIdField(),
getSchema()));
}
SQLHelper helper = new SQLHelper(sqlConnection, table, descriptor.getCreateTablePolicy());
boolean loadData = helper.setupTable();
return loadData;
} catch (SQLException e) {
// exception on close
throw new DirectoryException(e);
}
}
public Connection getConnection() throws DirectoryException {
SQLDirectoryDescriptor descriptor = getDescriptor();
if (StringUtils.isBlank(descriptor.dataSourceName)) {
throw new DirectoryException("Missing dataSource for SQL directory: " + getName());
}
try {
return ConnectionHelper.getConnection(descriptor.dataSourceName);
} catch (SQLException e) {
throw new DirectoryException("Cannot connect to SQL directory '" + getName() + "': " + e.getMessage(), e);
}
}
@Override
public Session getSession() throws DirectoryException {
boolean loadData = initConnectionIfNeeded();
SQLSession session = new SQLSession(this, getDescriptor());
addSession(session);
if (loadData && descriptor.getDataFileName() != null) {
Schema schema = Framework.getService(SchemaManager.class).getSchema(getSchema());
DirectoryCSVLoader.loadData(descriptor.getDataFileName(), descriptor.getDataFileCharacterSeparator(),
schema, session::createEntry);
}
return session;
}
protected void addSession(final SQLSession session) throws DirectoryException {
super.addSession(session);
registerInTx(session);
}
protected void registerInTx(final SQLSession session) throws DirectoryException {
if (!TransactionHelper.isTransactionActive()) {
return;
}
TransactionHelper.registerSynchronization(new TxSessionCleaner(session));
}
public Map<String, Field> getSchemaFieldMap() {
return schemaFieldMap;
}
public Table getTable() {
return table;
}
public Dialect getDialect() {
return dialect;
}
public boolean useNativeCase() {
return nativeCase;
}
@Override
public boolean isMultiTenant() {
return table.getColumn(TENANT_ID_FIELD) != null;
}
@Override
public String toString() {
return "SQLDirectory [name=" + descriptor.name + "]";
}
}